diff --git a/local-cli/core/windows/findDependencyProject.js b/local-cli/core/windows/findDependencyProject.js new file mode 100644 index 00000000000..9c1eb8e15c9 --- /dev/null +++ b/local-cli/core/windows/findDependencyProject.js @@ -0,0 +1,21 @@ +/** + * 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 findProjectWithFilePattern = require('./findProjectWithFilePattern'); + +/** + * Find a C# dependency project file + * + * @param {String} folder Name of the folder where to seek + * @return {String} + */ +module.exports = function findDependencyProject(folder) { + return findProjectWithFilePattern(folder, /class\s+\S+\s*:\s*IReactPackage/); +}; diff --git a/local-cli/core/windows/findMainFile.js b/local-cli/core/windows/findMainFile.js new file mode 100644 index 00000000000..e0bdfdb62c5 --- /dev/null +++ b/local-cli/core/windows/findMainFile.js @@ -0,0 +1,34 @@ +/** + * 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 the main file for the C# project + * + * @param {String} folder Name of the folder where to seek + * @return {String} + */ +module.exports = function findMainFile(folder) { + let mainFilePath = glob.sync('MainReactNativeHost.cs', { + cwd: folder, + ignore: ['node_modules/**', '**/build/**', 'Examples/**', 'examples/**'], + }); + + if (mainFilePath.length === 0) { + mainFilePath = glob.sync('MainPage.cs', { + cwd: folder, + ignore: ['node_modules/**', '**/build/**', 'Examples/**', 'examples/**'], + }); + } + + return mainFilePath && mainFilePath.length > 0 ? path.join(folder, mainFilePath[0]) : null; +}; diff --git a/local-cli/core/windows/findNamespace.js b/local-cli/core/windows/findNamespace.js new file mode 100644 index 00000000000..4867368f122 --- /dev/null +++ b/local-cli/core/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/windows/findPackageClassName.js b/local-cli/core/windows/findPackageClassName.js new file mode 100644 index 00000000000..279610eac7f --- /dev/null +++ b/local-cli/core/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/windows/findProject.js b/local-cli/core/windows/findProject.js new file mode 100644 index 00000000000..3e89b837c4a --- /dev/null +++ b/local-cli/core/windows/findProject.js @@ -0,0 +1,21 @@ +/** + * 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 findProjectWithFilePattern = require('./findProjectWithFilePattern'); + +/** + * Find a C# dependency project file + * + * @param {String} folder Name of the folder where to seek + * @return {String} + */ +module.exports = function findDependencyProject(folder) { + return findProjectWithFilePattern(folder, /class\s+\S+\s*:\s*(ReactNativeHost|ReactPage)/); +}; diff --git a/local-cli/core/windows/findProjectGUID.js b/local-cli/core/windows/findProjectGUID.js new file mode 100644 index 00000000000..06ae5a17042 --- /dev/null +++ b/local-cli/core/windows/findProjectGUID.js @@ -0,0 +1,22 @@ +/** + * 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'); + +/** + * Gets GUID of dependency project. + * + * @param {String} folder Path to dependency project .csproj + */ +module.exports = function findProjectGuid(path) { + const file = fs.readFileSync(path, 'utf8'); + const result = file.match(/\{(.*)\}<\/ProjectGuid>/); + return result ? result[1] : null; +}; diff --git a/local-cli/core/windows/findProjectName.js b/local-cli/core/windows/findProjectName.js new file mode 100644 index 00000000000..c689de9575e --- /dev/null +++ b/local-cli/core/windows/findProjectName.js @@ -0,0 +1,22 @@ +/** + * 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'); + +/** + * Gets name of dependency project. + * + * @param {String} folder Path to dependency project .csproj + */ +module.exports = function findProjectGuid(path) { + const file = fs.readFileSync(path, 'utf8'); + const result = file.match(/(.*)<\/AssemblyName>/); + return result ? result[1] : null; +}; diff --git a/local-cli/core/windows/findProjectWithFilePattern.js b/local-cli/core/windows/findProjectWithFilePattern.js new file mode 100644 index 00000000000..3e95a2005f1 --- /dev/null +++ b/local-cli/core/windows/findProjectWithFilePattern.js @@ -0,0 +1,47 @@ +/** + * 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'); + +/** + * Find an C# project file + * + * @param {String} folder Name of the folder where to seek + * @return {String} + */ +module.exports = function findProjectWithFilePattern(folder, pattern) { + const csprojPaths = glob.sync(path.join('**', '*.csproj'), { + cwd: folder, + ignore: ['node_modules/**', '**/build/**', 'Examples/**', 'examples/**', '**/bin/**', '**/obj/**' ], + }); + + // Search all folders with *.csproj files for one that exports an `IReactPackage` + for (var csprojPath of csprojPaths) { + const csprojFullPath = path.join(folder, csprojPath); + const csprojDir = path.dirname(csprojFullPath); + const files = glob.sync(path.join('**', '*.cs'), { + cwd: csprojDir, + ignore: ['node_modules/**', '**/build/**', 'Examples/**', 'examples/**', '**/bin/**', '**/obj/**' ], + }); + + const packages = files + .map(filePath => fs.readFileSync(path.join(csprojDir, filePath), 'utf8')) + .map(file => file.match(pattern)) + .filter(match => match); + + if (packages.length) { + return csprojFullPath; + } + } + + return null; +}; diff --git a/local-cli/core/windows/findWindowsSolution.js b/local-cli/core/windows/findWindowsSolution.js new file mode 100644 index 00000000000..fb6782633b9 --- /dev/null +++ b/local-cli/core/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/windows/index.js b/local-cli/core/windows/index.js new file mode 100644 index 00000000000..f403022bbe6 --- /dev/null +++ b/local-cli/core/windows/index.js @@ -0,0 +1,100 @@ +/** + * 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 findDependencyProject = require('./findDependencyProject'); +const findProjectName = require('./findProjectName'); +const findProjectGUID = require('./findProjectGUID'); +const findPackageClassName = require('./findPackageClassName'); +const findMainFile = require('./findMainFile'); +const path = require('path'); + +const relativeProjectPath = (fullProjPath) => { + const windowsPath = fullProjPath + .substring(fullProjPath.lastIndexOf("node_modules") - 1, fullProjPath.length) + .replace(/\//g, '\\'); + + return path.join('..', windowsPath); +}; + +/** + * 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 projectPath = userConfig.projectPath || findProject(folder); + + if (!csSolution || !projectPath) { + return null; + } + + // expects solutions to be named the same as project folders + const solutionPath = path.join(folder, csSolution); + const sourceDir = userConfig.sourceDir || path.dirname(projectPath); + const mainFilePath = findMainFile(sourceDir); + + return { + sourceDir, + solutionPath, + projectPath, + mainFilePath, + folder, + userConfig, + }; +}; + +/** + * Same as projectConfigWindows except it returns + * different config that applies to packages only + */ +exports.dependencyConfig = function dependencyConfigWindows(folder, userConfig) { + + const projectPath = userConfig.projectPath || findDependencyProject(folder); + + if (!projectPath) { + return null; + } + + const sourceDir = userConfig.sourceDir || path.dirname(projectPath); + const packageClassName = findPackageClassName(sourceDir); + const namespace = userConfig.namespace || findNamespace(sourceDir); + + /** + * 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 = findProjectGUID(projectPath); + const projectName = findProjectName(projectPath); + const relativeProjPath = relativeProjectPath(projectPath); + + return { + sourceDir, + packageUsingPath, + packageInstance, + projectName, + projectPath, + folder, + projectGUID, + relativeProjPath, + }; +}; diff --git a/local-cli/link/index.js b/local-cli/link/index.js new file mode 100644 index 00000000000..89ce37075fa --- /dev/null +++ b/local-cli/link/index.js @@ -0,0 +1,7 @@ +exports.linkConfig = function() { + return { + isInstalled: require('./windows/isInstalled'), + register: require('./windows/registerNativeModule'), + unregister: require('./windows/unregisterNativeModule') + }; +}; diff --git a/local-cli/link/windows/isInstalled.js b/local-cli/link/windows/isInstalled.js new file mode 100644 index 00000000000..b4edd1c8309 --- /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.mainFilePath) + .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 00000000000..21c1e9545bc --- /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 00000000000..81757076cbe --- /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/makeConfigurationPatch.js b/local-cli/link/windows/patches/makeConfigurationPatch.js new file mode 100644 index 00000000000..a6170b4fde7 --- /dev/null +++ b/local-cli/link/windows/patches/makeConfigurationPatch.js @@ -0,0 +1,30 @@ +module.exports = function makeSolutionPatch(windowsConfig) { + + const configs = { + 'Debug': 'Debug', + 'DebugBundle': 'Debug', + 'Release': 'Release', + 'ReleaseBundle': 'Release' + }; + + const platforms = [ 'ARM', 'x64', 'x86']; + + const entries = [ 'ActiveCfg', 'Build.0' ]; + + let solutionConfiguration = ''; + for (var appConfig in configs) { + for (var platform of platforms) { + for (var entry of entries) { + let projectConfig = configs[appConfig]; + solutionConfiguration += ` {${windowsConfig.projectGUID.toUpperCase()}}.${appConfig}|${platform}.${entry} = ${projectConfig}|${platform}\r\n`; + } + } + } + + /* eslint-disable no-control-regex */ + return { + pattern: new RegExp('[^\S\r\n]+{C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug'), + patch: solutionConfiguration, + unpatch: new RegExp(`.*{${windowsConfig.projectGUID.toUpperCase()}}.*[\r\n]*`, 'gi'), + }; +}; diff --git a/local-cli/link/windows/patches/makePackagePatch.js b/local-cli/link/windows/patches/makePackagePatch.js new file mode 100644 index 00000000000..e1a5bec5df3 --- /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 00000000000..e62cd6b7b89 --- /dev/null +++ b/local-cli/link/windows/patches/makeProjectPatch.js @@ -0,0 +1,15 @@ +module.exports = function makeProjectPatch(windowsConfig) { + + const projectInsert = ` + {${windowsConfig.projectGUID}} + ${windowsConfig.projectName} + + `; + + /* eslint-disable no-control-regex */ + return { + pattern: '', + patch: projectInsert, + unpatch: new RegExp(`[^\S\r\n]+