feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,412 @@
import groovy.json.JsonSlurper
import java.nio.file.Paths
// Object representing a gradle project.
class ExpoModuleGradleProject {
// Name of the Android project
String name
// Path to the folder with Android project
String sourceDir
ExpoModuleGradleProject(Object data) {
this.name = data.name
this.sourceDir = data.sourceDir
}
}
// Object representing a gradle plugin
class ExpoModuleGradlePlugin {
// ID of the gradle plugin
String id
// Artifact group
String group
// Path to the plugin folder
String sourceDir
ExpoModuleGradlePlugin(Object data) {
this.id = data.id
this.group = data.group
this.sourceDir = data.sourceDir
}
}
// Object representing a module.
class ExpoModule {
// Name of the JavaScript package
String name
// Version of the package, loaded from `package.json`
String version
// Gradle projects
ExpoModuleGradleProject[] projects
// Gradle plugins
ExpoModuleGradlePlugin[] plugins
ExpoModule(Object data) {
this.name = data.packageName
this.version = data.packageVersion
this.projects = data.projects.collect { new ExpoModuleGradleProject(it) }
this.plugins = data.plugins.collect { new ExpoModuleGradlePlugin(it) }
}
}
// Object representing a maven repository.
class MavenRepo {
String url
Object credentials
String authentication
MavenRepo(Object data) {
this.url = data.url
this.credentials = data.credentials
this.authentication = data.authentication
}
}
class ExpoAutolinkingManager {
private File projectDir
private Map options
private Object cachedResolvingResults
static String generatedPackageListNamespace = 'expo.modules'
static String generatedPackageListFilename = 'ExpoModulesPackageList.java'
static String generatedFilesSrcDir = 'generated/expo/src/main/java'
ExpoAutolinkingManager(File projectDir, Map options = [:]) {
this.projectDir = projectDir
this.options = options
}
Object resolve(ProviderFactory providers, shouldUseCachedValue=false) {
if (cachedResolvingResults) {
return cachedResolvingResults
}
if (shouldUseCachedValue) {
logger.warn("Warning: Expo modules were resolved multiple times. Probably something is wrong with the project configuration.")
}
String[] args = convertOptionsToCommandArgs('resolve', this.options)
args += ['--json']
String output = providers.exec {
workingDir(projectDir)
commandLine(args)
}.standardOutput.asText.get().trim()
Object json = new JsonSlurper().parseText(output)
cachedResolvingResults = json
return json
}
boolean shouldUseAAR() {
return options?.useAAR == true
}
ExpoModule[] getModules(ProviderFactory providers, shouldUseCachedValue=false) {
Object json = resolve(providers, shouldUseCachedValue)
return json.modules.collect { new ExpoModule(it) }
}
MavenRepo[] getExtraMavenRepos(ProviderFactory providers, shouldUseCachedValue=false) {
Object json = resolve(providers, shouldUseCachedValue)
return json.extraDependencies.collect { new MavenRepo(it) }
}
static String getGeneratedFilePath(Project project) {
return Paths.get(
project.buildDir.toString(),
generatedFilesSrcDir,
generatedPackageListNamespace.replace('.', '/'),
generatedPackageListFilename
).toString()
}
static void generatePackageList(Project project, Map options) {
String[] args = convertOptionsToCommandArgs('generate-package-list', options)
// Construct absolute path to generated package list.
def generatedFilePath = getGeneratedFilePath(project)
args += [
'--namespace',
generatedPackageListNamespace,
'--target',
generatedFilePath
]
if (options == null) {
// Options are provided only when settings.gradle was configured.
// If not or opted-out from autolinking, the generated list should be empty.
args += '--empty'
}
project.providers.exec {
workingDir(project.rootDir)
commandLine(args)
}.result.get().assertNormalExitValue()
}
static private String[] convertOptionsToCommandArgs(String command, Map options) {
String[] args = [
'node',
'--no-warnings',
'--eval',
// Resolve the `expo` > `expo-modules-autolinking` chain from the project root
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo\')] }))(process.argv.slice(1))',
'--',
command,
'--platform',
'android'
]
def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null))
if (searchPaths) {
args += searchPaths
}
if (options?.ignorePaths) {
args += '--ignore-paths'
args += options.ignorePaths
}
if (options?.exclude) {
args += '--exclude'
args += options.exclude
}
return args
}
}
class Colors {
static final String GREEN = "\u001B[32m"
static final String YELLOW = "\u001B[33m"
static final String RESET = "\u001B[0m"
}
class Emojis {
static final String INFORMATION = "\u2139\uFE0F"
}
// We can't cast a manager that is created in `settings.gradle` to the `ExpoAutolinkingManager`
// because if someone is using `buildSrc`, the `ExpoAutolinkingManager` class
// will be loaded by two different class loader - `settings.gradle` will use a diffrent loader.
// In the JVM, classes are equal only if were loaded by the same loader.
// There is nothing that we can do in that case, but to make our code safer, we check if the class name is the same.
def validateExpoAutolinkingManager(manager) {
assert ExpoAutolinkingManager.name == manager.getClass().name
return manager
}
// Here we split the implementation, depending on Gradle context.
// `rootProject` is a `ProjectDescriptor` if this file is imported in `settings.gradle` context,
// otherwise we can assume it is imported in `build.gradle`.
if (rootProject instanceof ProjectDescriptor) {
// Method to be used in `settings.gradle`. Options passed here will have an effect in `build.gradle` context as well,
// i.e. adding the dependencies and generating the package list.
ext.useExpoModules = { Map options = [:] ->
ExpoAutolinkingManager manager = new ExpoAutolinkingManager(rootProject.projectDir, options)
ExpoModule[] modules = manager.getModules(providers)
MavenRepo[] extraMavenRepos = manager.getExtraMavenRepos(providers)
for (module in modules) {
for (moduleProject in module.projects) {
include(":${moduleProject.name}")
project(":${moduleProject.name}").projectDir = new File(moduleProject.sourceDir)
}
for (modulePlugin in module.plugins) {
includeBuild(new File(modulePlugin.sourceDir))
}
}
gradle.beforeProject { project ->
if (project !== project.rootProject) {
return
}
def rootProject = project
// Add plugin classpath to the root project
for (module in modules) {
for (modulePlugin in module.plugins) {
rootProject.buildscript.dependencies.add('classpath', "${modulePlugin.group}:${modulePlugin.id}")
}
}
// Add extra maven repositories to allprojects
for (mavenRepo in extraMavenRepos) {
println "Adding extra maven repository - '${mavenRepo.url}'"
}
rootProject.allprojects { eachProject ->
eachProject.repositories {
for (mavenRepo in extraMavenRepos) {
maven {
url "${mavenRepo.url}"
if (mavenRepo.credentials != null) {
credentials {
if (mavenRepo.credentials.username && mavenRepo.credentials.password) {
username mavenRepo.credentials.username
password mavenRepo.credentials.password
} else if (mavenRepo.credentials.name && mavenRepo.credentials.value) {
name mavenRepo.credentials.name
value mavenRepo.credentials.value
} else if (mavenRepo.credentials.accessKey && mavenRepo.credentials.secretKey) {
accessKey mavenRepo.credentials.accessKey
secretKey mavenRepo.credentials.secretKey
sessionToken mavenRepo.credentials.sessionToken
}
}
}
if (mavenRepo.authentication != null) {
authentication {
if (mavenRepo.authentication == "basic") {
basic(BasicAuthentication)
} else if (mavenRepo.authentication == "digest") {
digest(DigestAuthentication)
} else if (mavenRepo.authentication == "header") {
header(HttpHeaderAuthentication)
}
}
}
}
}
}
}
}
// Apply plugins for all app projects
gradle.afterProject { project ->
if (!project.plugins.hasPlugin('com.android.application')) {
return
}
for (module in modules) {
for (modulePlugin in module.plugins) {
println " ${Emojis.INFORMATION} ${Colors.YELLOW}Applying gradle plugin${Colors.RESET} '${Colors.GREEN}${modulePlugin.id}${Colors.RESET}' (${module.name}@${module.version})"
project.plugins.apply(modulePlugin.id)
}
}
}
// Save the manager in the shared context, so that we can later use it in `build.gradle`.
gradle.ext.expoAutolinkingManager = manager
}
} else {
def addModule = { DependencyHandler handler, String projectName, Boolean useAAR ->
Project dependency = rootProject.project(":${projectName}")
if (useAAR) {
handler.add('api', "${dependency.group}:${projectName}:${dependency.version}")
} else {
handler.add('api', dependency)
}
}
def addDependencies = { DependencyHandler handler, Project project ->
def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager)
def modules = manager.getModules(project.providers, true)
if (!modules.length) {
return
}
println ''
println 'Using expo modules'
for (module in modules) {
// Don't link itself
if (module.name == project.name) {
continue
}
// Can remove this once we move all the interfaces into the core.
if (module.name.endsWith('-interface')) {
continue
}
for (moduleProject in module.projects) {
addModule(handler, moduleProject.name, manager.shouldUseAAR())
println " - ${Colors.GREEN}${moduleProject.name}${Colors.RESET} (${module.version})"
}
}
println ''
}
// Adding dependencies
ext.addExpoModulesDependencies = { DependencyHandler handler, Project project ->
// Return early if `useExpoModules` was not called in `settings.gradle`
if (!gradle.ext.has('expoAutolinkingManager')) {
logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.')
return
}
def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager)
if (rootProject.findProject(':expo-modules-core')) {
// `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off.
addModule(handler, 'expo-modules-core', manager.shouldUseAAR())
} else {
logger.error('Error: `expo-modules-core` project is not included by autolinking.')
}
// If opted-in not to autolink modules as dependencies
if (manager.options == null) {
return
}
addDependencies(handler, project)
}
// Generating the package list
ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir
ext.generateExpoModulesPackageList = {
// Get options used in `settings.gradle` or null if it wasn't set up.
Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null
if (options == null) {
// TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2
// logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.')
}
ExpoAutolinkingManager.generatePackageList(project, options)
}
ext.getGenerateExpoModulesPackagesListPath = {
return ExpoAutolinkingManager.getGeneratedFilePath(project)
}
ext.getModulesConfig = {
if (!gradle.ext.has('expoAutolinkingManager')) {
return null
}
def modules = gradle.ext.expoAutolinkingManager.resolve(project.providers, true).modules
return modules.toString()
}
ext.ensureDependeciesWereEvaluated = { Project project ->
if (!gradle.ext.has('expoAutolinkingManager')) {
return
}
def modules = gradle.ext.expoAutolinkingManager.getModules(project.providers, true)
for (module in modules) {
for (moduleProject in module.projects) {
def dependency = project.findProject(":${moduleProject.name}")
if (dependency == null) {
logger.warn("Coudn't find project ${moduleProject.name}. Please, make sure that `useExpoModules` was called in `settings.gradle`.")
continue
}
// Prevent circular dependencies
if (moduleProject.name == project.name) {
continue
}
project.evaluationDependsOn(":${moduleProject.name}")
}
}
}
}

View File

@@ -0,0 +1,255 @@
require_relative 'constants'
require_relative 'package'
# Require extensions to CocoaPods' classes
require_relative 'cocoapods/pod_target'
require_relative 'cocoapods/sandbox'
require_relative 'cocoapods/target_definition'
require_relative 'cocoapods/umbrella_header_generator'
require_relative 'cocoapods/user_project_integrator'
module Expo
class AutolinkingManager
require 'colored2'
include Pod
public def initialize(podfile, target_definition, options)
@podfile = podfile
@target_definition = target_definition
@options = options
validate_target_definition()
resolve_result = resolve()
@packages = resolve_result['modules'].map { |json_package| Package.new(json_package) }
@extraPods = resolve_result['extraDependencies']
end
public def use_expo_modules!
if has_packages?
return
end
global_flags = @options.fetch(:flags, {})
tests_only = @options.fetch(:testsOnly, false)
include_tests = @options.fetch(:includeTests, false)
project_directory = Pod::Config.instance.project_root
UI.section 'Using Expo modules' do
@packages.each { |package|
package.pods.each { |pod|
# The module can already be added to the target, in which case we can just skip it.
# This allows us to add a pod before `use_expo_modules` to provide custom flags.
if @target_definition.dependencies.any? { |dependency| dependency.name == pod.pod_name }
UI.message '— ' << package.name.green << ' is already added to the target'.yellow
next
end
# Skip if the podspec doesn't include the platform for the current target.
unless pod.supports_platform?(@target_definition.platform)
UI.message '- ' << package.name.green << " doesn't support #{@target_definition.platform.string_name} platform".yellow
next
end
# Ensure that the dependencies of packages with Swift code use modular headers, otherwise
# `pod install` may fail if there is no `use_modular_headers!` declaration or
# `:modular_headers => true` is not used for this particular dependency.
# The latter require adding transitive dependencies to user's Podfile that we'd rather like to avoid.
if package.has_something_to_link?
use_modular_headers_for_dependencies(pod.spec.all_dependencies)
end
podspec_dir_path = Pathname.new(pod.podspec_dir).relative_path_from(project_directory).to_path
debug_configurations = @target_definition.build_configurations ? @target_definition.build_configurations.select { |config| config.include?('Debug') }.keys : ['Debug']
pod_options = {
:path => podspec_dir_path,
:configuration => package.debugOnly ? debug_configurations : [] # An empty array means all configurations
}.merge(global_flags, package.flags)
if tests_only || include_tests
test_specs_names = pod.spec.test_specs.map { |test_spec|
test_spec.name.delete_prefix(pod.spec.name + "/")
}
# Jump to the next package when it doesn't have any test specs (except interfaces, they're required)
# TODO: Can remove interface check once we move all the interfaces into the core.
next if tests_only && test_specs_names.empty? && !pod.pod_name.end_with?('Interface')
pod_options[:testspecs] = test_specs_names
end
# Install the pod.
@podfile.pod(pod.pod_name, pod_options)
# TODO: Can remove this once we move all the interfaces into the core.
next if pod.pod_name.end_with?('Interface')
UI.message "#{package.name.green} (#{package.version})"
}
}
end
@extraPods.each { |pod|
UI.info "Adding extra pod - #{pod['name']} (#{pod['version'] || '*'})"
requirements = Array.new
requirements << pod['version'] if pod['version']
options = Hash.new
options[:configurations] = pod['configurations'] if pod['configurations']
options[:modular_headers] = pod['modular_headers'] if pod['modular_headers']
options[:source] = pod['source'] if pod['source']
options[:path] = pod['path'] if pod['path']
options[:podspec] = pod['podspec'] if pod['podspec']
options[:testspecs] = pod['testspecs'] if pod['testspecs']
options[:git] = pod['git'] if pod['git']
options[:branch] = pod['branch'] if pod['branch']
options[:tag] = pod['tag'] if pod['tag']
options[:commit] = pod['commit'] if pod['commit']
requirements << options
@podfile.pod(pod['name'], *requirements)
}
self
end
# Spawns `expo-module-autolinking generate-modules-provider` command.
public def generate_modules_provider(target_name, target_path)
Process.wait IO.popen(generate_modules_provider_command_args(target_path)).pid
end
# If there is any package to autolink.
public def has_packages?
@packages.empty?
end
# Filters only these packages that needs to be included in the generated modules provider.
public def packages_to_generate
platform = @target_definition.platform
@packages.select do |package|
# Check whether the package has any module to autolink
# and if there is any pod that supports target's platform.
package.has_something_to_link? && package.pods.any? { |pod| pod.supports_platform?(platform) }
end
end
# Returns the provider name which is also a name of the generated file
public def modules_provider_name
@options.fetch(:providerName, Constants::MODULES_PROVIDER_FILE_NAME)
end
# Absolute path to `Pods/Target Support Files/<pods target name>/<modules provider file>` within the project path
public def modules_provider_path(target)
File.join(target.support_files_dir, modules_provider_name)
end
# For now there is no need to generate the modules provider for testing.
public def should_generate_modules_provider?
return !@options.fetch(:testsOnly, false)
end
# Returns the platform name of the current target definition.
# Note that it is suitable to be presented to the user (i.e. is not lowercased).
public def platform_name
return @target_definition.platform&.string_name
end
# privates
private def resolve
json = []
IO.popen(resolve_command_args) do |data|
while line = data.gets
json << line
end
end
begin
JSON.parse(json.join())
rescue => error
raise "Couldn't parse JSON coming from `expo-modules-autolinking` command:\n#{error}"
end
end
public def base_command_args
search_paths = @options.fetch(:searchPaths, @options.fetch(:modules_paths, nil))
ignore_paths = @options.fetch(:ignorePaths, nil)
exclude = @options.fetch(:exclude, [])
args = []
if !search_paths.nil? && !search_paths.empty?
args.concat(search_paths)
end
if !ignore_paths.nil? && !ignore_paths.empty?
args.concat(['--ignore-paths'], ignore_paths)
end
if !exclude.nil? && !exclude.empty?
args.concat(['--exclude'], exclude)
end
args
end
private def node_command_args(command_name)
eval_command_args = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [\'' + __dir__ + '\'] }))(process.argv.slice(1))',
command_name,
'--platform',
'apple'
]
return eval_command_args.concat(base_command_args())
end
private def resolve_command_args
node_command_args('resolve').concat(['--json'])
end
public def generate_modules_provider_command_args(target_path)
node_command_args('generate-modules-provider').concat(
[
'--target',
target_path,
'--packages'
],
packages_to_generate.map(&:name)
)
end
private def use_modular_headers_for_dependencies(dependencies)
dependencies.each { |dependency|
# The dependency name might be a subspec like `ReactCommon/turbomodule/core`,
# but the modular headers need to be enabled for the entire `ReactCommon` spec anyway,
# so we're stripping the subspec path from the dependency name.
root_spec_name = dependency.name.partition('/').first
unless @target_definition.build_pod_as_module?(root_spec_name)
UI.info "[Expo] ".blue << "Enabling modular headers for pod #{root_spec_name.green}"
# This is an equivalent to setting `:modular_headers => true` for the specific dependency.
@target_definition.set_use_modular_headers_for_pod(root_spec_name, true)
end
}
end
# Validates whether the Expo modules can be autolinked in the given target definition.
private def validate_target_definition
# The platform must be declared within the current target (e.g. `platform :ios, '13.0'`)
if platform_name.nil?
raise "Undefined platform for target #{@target_definition.name}, make sure to call `platform` method globally or inside the target"
end
# The declared platform must be iOS, macOS or tvOS, others are not supported.
unless ['iOS', 'macOS', 'tvOS'].include?(platform_name)
raise "Target #{@target_definition.name} is dedicated to #{platform_name} platform, which is not supported by Expo Modules"
end
end
end # class AutolinkingManager
end # module Expo

View File

@@ -0,0 +1,53 @@
module Pod
class PodTarget
private
_original_module_map_path = instance_method(:module_map_path)
public
# CocoaPods's default modulemap did not generate submodules correctly
# `ios/Pods/Headers/Public/React/React-Core.modulemap`
# ```
# module React {
# umbrella header "React-Core-umbrella.h"
#
# export *
# module * { export * }
# }
# ```
# clang will generate submodules for headers relative to the umbrella header directory.
# https://github.com/llvm/llvm-project/blob/2782cb8da0b3c180fa7c8627cb255a026f3d25a2/clang/lib/Lex/ModuleMap.cpp#L1133
# In this case, it is `ios/Pods/Headers/Public/React`.
# But the React public headers are placed in `ios/Pods/Headers/Public/React-Core/React`, so clang cannot find the headers and generate submodules.
#
# This case happens when a pod's name different to its module name, e.g. the pod name is `React-Core` but the module name is `React` since it defines header_dir as `React`.
# To fix the issue, we rewrite the `module_map_path` and `umbrella_header_path` to be with the public headers,
# i.e. `ios/Pods/Headers/Public/React-Core/React/React-Core.modulemap` and `ios/Pods/Headers/Public/React-Core/React/React-Core-umbrella.h`
#
def rewrite_module_dir
# strip expo go versioning prefix
normalized_name = name.gsub(/^ABI\d+_\d+_\d+/, '')
if ['React-Core', 'React-RCTFabric'].include?(normalized_name) && product_module_name != name
return sandbox.public_headers.root + name + product_module_name
end
return nil
end
def umbrella_header_path
if dir = self.rewrite_module_dir
return dir + "#{label}-umbrella.h"
end
super
end
define_method(:module_map_path) do
if dir = self.rewrite_module_dir
return dir + "#{label}.modulemap"
end
_original_module_map_path.bind(self).()
end
end # class PodTarget
end # module Pod

View File

@@ -0,0 +1,68 @@
# Overrides CocoaPods `Sandbox` class to patch podspecs on the fly
# See: https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/sandbox.rb
require 'json'
REACT_DEFINE_MODULES_LIST = [
'ReactCommon',
'React-RCTAppDelegate',
'React-hermes',
'React-jsc',
'React-Fabric',
'React-graphics',
'React-utils',
'React-debug',
]
module Pod
class Sandbox
private
_original_store_podspec = instance_method(:store_podspec)
public
define_method(:store_podspec) do |name, podspec, _external_source, json|
spec = _original_store_podspec.bind(self).(name, podspec, _external_source, json)
patched_spec = nil
# Patch `React-Core.podspec` for clang to generate correct submodules for swift integration
if name == 'React-Core'
spec_json = JSON.parse(spec.to_pretty_json)
# clang module does not support objc++.
# We should put Hermes headers inside private headers directory.
# Otherwise, clang will throw errors in building module.
hermes_subspec_index = spec_json['subspecs'].index { |subspec| subspec['name'] == 'Hermes' }
if hermes_subspec_index
spec_json['subspecs'][hermes_subspec_index]['private_header_files'] ||= [
'ReactCommon/hermes/executor/*.h',
'ReactCommon/hermes/inspector/*.h',
'ReactCommon/hermes/inspector/chrome/*.h',
'ReactCommon/hermes/inspector/detail/*.h',
]
end
patched_spec = Specification.from_json(spec_json.to_json)
# Patch podspecs to define module
elsif REACT_DEFINE_MODULES_LIST.include? name
spec_json = JSON.parse(podspec.to_pretty_json)
spec_json['pod_target_xcconfig'] ||= {}
spec_json['pod_target_xcconfig']['DEFINES_MODULE'] = 'YES'
patched_spec = Specification.from_json(spec_json.to_json)
end
if patched_spec != nil
# Store the patched spec with original checksum and local saved file path
patched_spec.defined_in_file = spec.defined_in_file
patched_spec.instance_variable_set(:@checksum, spec.checksum)
@stored_podspecs[spec.name] = patched_spec
return patched_spec
end
return spec
end # define_method(:store_podspec)
end # class Sandbox
end # module Pod

View File

@@ -0,0 +1,22 @@
# Overrides CocoaPods class as the AutolinkingManager is in fact part of
# the target definitions and we need to refer to it at later steps.
# See: https://github.com/CocoaPods/Core/blob/master/lib/cocoapods-core/podfile/target_definition.rb
module Pod
class Podfile
class TargetDefinition
public
attr_writer :autolinking_manager
def autolinking_manager
if @autolinking_manager.present? || root?
@autolinking_manager
else
parent.autolinking_manager
end
end
end
end
end

View File

@@ -0,0 +1,22 @@
module Pod
module Generator
class UmbrellaHeader
private
_original_generate = instance_method(:generate)
public
define_method (:generate) do
if self.target.is_a?(Pod::PodTarget) && self.target.rewrite_module_dir
# If we write the `umbrella_header_path`, the import headers are in the same directory,
# e.g. `#import "React/RCTBridge.h"` -> `#import "RCTBridge.h"`
self.imports = self.imports.map { |import| import.basename }
end
_original_generate.bind(self).()
end
end # class UmbrellaHeader
end # module Generator
end # module Pod

View File

@@ -0,0 +1,62 @@
require_relative '../project_integrator'
# Unfortunately there is no good and official place that we could use to generate module providers
# and integrate them with user targets by operating on the PBXProj kept and saved by CocoaPods.
# So we have to hook into the private method called `integrate_user_targets`
# where we have public access to everything that we need.
# Original implementation: https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/installer/user_project_integrator.rb
module Pod
class Installer
class UserProjectIntegrator
include Expo::ProjectIntegrator
private
_original_integrate_user_targets = instance_method(:integrate_user_targets)
# Integrates the targets of the user projects with the libraries
# generated from the {Podfile}.
#
# @note {TargetDefinition} without dependencies are skipped to prevent
# creating empty libraries for target definitions which are only
# wrappers for others.
#
# @return [void]
#
define_method(:integrate_user_targets) do
# Call original method first
results = _original_integrate_user_targets.bind(self).()
UI.message '- Integrating Expo modules providers' do
# All user targets mapped to user projects.
all_projects = targets.map { |target| target.user_project }.uniq
# Array of projects to integrate is usually a subset of `all_projects`,
# and it might be empty subsequent installs after the first install.
# CocoaPods integrates only these ones.
projects_to_integrate = user_projects_to_integrate()
# However, we need to make sure that all projects are integrated,
# regardless of the CocoaPods cache.
all_projects.each do |project|
project_targets = targets.select { |target| target.user_project.equal?(project) }
Expo::ProjectIntegrator::integrate_targets_in_project(project_targets, project)
Expo::ProjectIntegrator::remove_nils_from_source_files(project)
Expo::ProjectIntegrator::set_autolinking_configuration(project)
# CocoaPods saves the projects to integrate at the next step,
# but in some cases we're modifying other projects as well.
# Below we make sure the project will be saved and no more than once!
if project.dirty? && !projects_to_integrate.include?(project)
save_projects([project])
end
end
end
results
end
end # class UserProjectIntegrator
end # class Installer
end # module Pod

View File

@@ -0,0 +1,9 @@
module Expo
module Constants
GENERATED_GROUP_NAME = 'ExpoModulesProviders'
MODULES_PROVIDER_FILE_NAME = 'ExpoModulesProvider.swift'
CONFIGURE_PROJECT_BUILD_SCRIPT_NAME = '[Expo] Configure project'
CONFIGURE_PROJECT_SCRIPT_FILE_NAME = 'expo-configure-project.sh'
end
end

View File

@@ -0,0 +1,80 @@
module Expo
class PackagePod
# Name of the pod
attr_reader :pod_name
# The directory where the podspec is
attr_reader :podspec_dir
# Specification of the pod.
attr_reader :spec
def initialize(json)
@pod_name = json['podName']
@podspec_dir = json['podspecDir']
@spec = get_podspec_for_pod(self)
end
# Checks whether the podspec declares support for the given platform.
# It compares not only the platform name, but also the deployment target.
def supports_platform?(platform)
return platform && @spec.available_platforms().any? do |available_platform|
next platform.supports?(available_platform)
end
end
end # class PackagePod
class Package
# Name of the npm package
attr_reader :name
# Version of the npm package
attr_reader :version
# An array of pods found in the package
attr_reader :pods
# Flags to pass to the pod definition
attr_reader :flags
# Class names of the modules that need to be included in the generated modules provider.
attr_reader :modules
# Whether this module should only be added to the debug configuration.
attr_reader :debugOnly
# Names of Swift classes that hooks into `ExpoAppDelegate` to receive AppDelegate life-cycle events.
attr_reader :appDelegateSubscribers
# Names of Swift classes that implement `ExpoReactDelegateHandler` to hook React instance creation.
attr_reader :reactDelegateHandlers
def initialize(json)
@name = json['packageName']
@version = json['packageVersion']
@pods = json['pods'].map { |pod| PackagePod.new(pod) }
@flags = json.fetch('flags', {})
@modules = json.fetch('modules', [])
@debugOnly = json['debugOnly']
@appDelegateSubscribers = json.fetch('appDelegateSubscribers', [])
@reactDelegateHandlers = json.fetch('reactDelegateHandlers', [])
end
# Returns a boolean value whether the package has any module, app delegate subscriber or react delegate handler to link.
def has_something_to_link?
return !@modules.empty? || !@appDelegateSubscribers.empty? || !@reactDelegateHandlers.empty?
end
end # class Package
end # module Expo
private def get_podspec_for_pod(pod)
podspec_file_path = File.join(pod.podspec_dir, pod.pod_name + ".podspec")
return Pod::Specification.from_file(podspec_file_path)
end

View File

@@ -0,0 +1,274 @@
require 'fileutils'
require 'colored2'
module Expo
module ProjectIntegrator
include Pod
CONFIGURATION_FLAG_PREFIX = 'EXPO_CONFIGURATION_'
SWIFT_FLAGS = 'OTHER_SWIFT_FLAGS'
# Integrates targets in the project and generates modules providers.
def self.integrate_targets_in_project(targets, project)
# Find the targets that use expo modules and need the modules provider
targets_with_modules_provider = targets.select do |target|
autolinking_manager = target.target_definition.autolinking_manager
autolinking_manager.present? && autolinking_manager.should_generate_modules_provider?
end
# Find existing PBXGroup for modules providers.
generated_group = modules_providers_group(project, targets_with_modules_provider.any?)
# Return early when the modules providers group has not been auto-created in the line above.
return if generated_group.nil?
# Remove existing groups for targets without modules provider.
generated_group.groups.each do |group|
# Remove the group if there is no target for this group.
if targets.none? { |target| target.target_definition.name == group.name && targets_with_modules_provider.include?(target) }
recursively_remove_group(group)
end
end
targets_with_modules_provider.sort_by(&:name).each do |target|
# The user target name (without `Pods-` prefix which is a part of `target.name`)
target_name = target.target_definition.name
# PBXNativeTarget of the user target
native_target = project.native_targets.find { |native_target| native_target.name == target_name }
# Shorthand ref for the autolinking manager.
autolinking_manager = target.target_definition.autolinking_manager
UI.message '- Generating the provider for ' << target_name.green << ' target' do
# Get the absolute path to the modules provider
modules_provider_path = autolinking_manager.modules_provider_path(target)
# Run `expo-modules-autolinking` command to generate the file
autolinking_manager.generate_modules_provider(target_name, modules_provider_path)
# PBXGroup for generated files per target
generated_target_group = generated_group.find_subpath(target_name, true)
# PBXGroup uses relative paths, so we need to strip the absolute path
modules_provider_relative_path = Pathname.new(modules_provider_path).relative_path_from(generated_target_group.real_path).to_s
if generated_target_group.find_file_by_path(modules_provider_relative_path).nil?
# Create new PBXFileReference if the modules provider is not in the group yet
modules_provider_file_reference = generated_target_group.new_file(modules_provider_path)
if native_target.source_build_phase.files_references.find { |ref| ref.present? && ref.path == modules_provider_relative_path }.nil?
# Put newly created PBXFileReference to the source files of the native target
native_target.add_file_references([modules_provider_file_reference])
project.mark_dirty!
end
end
end
integrate_build_script(autolinking_manager, project, target, native_target)
end
# Remove the generated group if it has nothing left inside
if targets_with_modules_provider.empty?
recursively_remove_group(generated_group)
end
end
def self.recursively_remove_group(group)
return if group.nil?
UI.message '- Removing ' << group.name.green << ' group' do
group.recursive_children.each do |child|
UI.message ' - Removing a reference to ' << child.name.green
child.remove_from_project
end
group.remove_from_project
group.project.mark_dirty!
end
end
# CocoaPods doesn't properly remove file references from the build phase
# They appear as nils and it's safe to just delete them from native targets
def self.remove_nils_from_source_files(project)
project.native_targets.each do |native_target|
native_target.source_build_phase.files.each do |build_file|
next unless build_file.file_ref.nil?
build_file.remove_from_project
project.mark_dirty!
end
end
end
def self.modules_providers_group(project, autocreate = false)
project.main_group.find_subpath(Constants::GENERATED_GROUP_NAME, autocreate)
end
# Sets EXPO_CONFIGURATION_* compiler flag for Swift.
def self.set_autolinking_configuration(project)
project.native_targets.each do |native_target|
native_target.build_configurations.each do |build_configuration|
configuration_flag = "-D #{CONFIGURATION_FLAG_PREFIX}#{build_configuration.debug? ? "DEBUG" : "RELEASE"}"
build_settings = build_configuration.build_settings
# For some targets it might be `nil` by default which is an equivalent to `$(inherited)`
if build_settings[SWIFT_FLAGS].nil?
build_settings[SWIFT_FLAGS] ||= '$(inherited)'
end
# If the correct flag is not set yet
if !build_settings[SWIFT_FLAGS].include?(configuration_flag)
# Remove existing flag to make sure we don't put another one each time
build_settings[SWIFT_FLAGS] = build_settings[SWIFT_FLAGS].gsub(/\b-D\s+#{Regexp.quote(CONFIGURATION_FLAG_PREFIX)}\w+/, '')
# Add the correct flag
build_settings[SWIFT_FLAGS] << ' ' << configuration_flag
# Make sure the project will be saved as we did some changes
project.mark_dirty!
end
end
end
end
# Makes sure that the build script configuring the project is installed,
# is up-to-date and is placed before the "Compile Sources" phase.
def self.integrate_build_script(autolinking_manager, project, target, native_target)
build_phases = native_target.build_phases
modules_provider_path = autolinking_manager.modules_provider_path(target)
# Look for our own build script phase
xcode_build_script = native_target.shell_script_build_phases.find { |script|
script.name == Constants::CONFIGURE_PROJECT_BUILD_SCRIPT_NAME
}
if xcode_build_script.nil?
# Inform the user that we added a build script.
puts "[Expo] ".blue << "Installing the build script for target " << native_target.name.green
# Create a new build script in the target, it's added as the last phase
xcode_build_script = native_target.new_shell_script_build_phase(Constants::CONFIGURE_PROJECT_BUILD_SCRIPT_NAME)
end
# Make sure it is before the "Compile Sources" build phase
xcode_build_script_index = build_phases.find_index(xcode_build_script)
compile_sources_index = build_phases.find_index { |phase|
phase.is_a?(Xcodeproj::Project::PBXSourcesBuildPhase)
}
if xcode_build_script_index.nil?
# This is almost impossible to get here as the script was just created with `new_shell_script_build_phase`
# that puts the script at the end of the phases, but let's log it just in case.
puts "[Expo] ".blue << "Unable to find the configuring build script in the Xcode project".red
end
if compile_sources_index.nil?
# In this case the project will probably not compile but that's not our fault
# and it doesn't block us from updating our build script.
puts "[Expo] ".blue << "Unable to find the compilation build phase in the Xcode project".red
end
# Insert our script before the "Compile Sources" phase when necessary
unless compile_sources_index.nil? || xcode_build_script_index < compile_sources_index
build_phases.insert(
compile_sources_index,
build_phases.delete_at(xcode_build_script_index)
)
end
# Get path to the script that will be added to the target support files
support_script_path = File.join(target.support_files_dir, Constants::CONFIGURE_PROJECT_SCRIPT_FILE_NAME)
support_script_relative_path = Pathname.new(support_script_path).relative_path_from(project.project_dir)
# Write to the shell script so it's always in-sync with the autolinking configuration
IO.write(
support_script_path,
generate_support_script(autolinking_manager, modules_provider_path)
)
# Make the support script executable
FileUtils.chmod('+x', support_script_path)
# Force the build phase script to run on each build (including incremental builds)
xcode_build_script.always_out_of_date = '1'
# Make sure the build script in Xcode is up to date, but probably it's not going to change
# as it just runs the script generated in the target support files
xcode_build_script.shell_script = generate_xcode_build_script(support_script_relative_path)
end
# Generates the shell script of the build script phase.
# Try not to modify this since it involves changes in the pbxproj so
# it's better to modify the support script instead, if possible.
def self.generate_xcode_build_script(script_relative_path)
escaped_path = script_relative_path.to_s.gsub(/[^a-zA-Z0-9,\._\+@%\/\-]/) { |char| "\\#{char}" }
<<~XCODE_BUILD_SCRIPT
# This script configures Expo modules and generates the modules provider file.
bash -l -c "./#{escaped_path}"
XCODE_BUILD_SCRIPT
end
# Generates the support script that is executed by the build script phase.
def self.generate_support_script(autolinking_manager, modules_provider_path)
args = autolinking_manager.base_command_args.map { |arg| "\"#{arg}\"" }
platform = autolinking_manager.platform_name.downcase
package_names = autolinking_manager.packages_to_generate.map { |package| "\"#{package.name}\"" }
<<~SUPPORT_SCRIPT
#!/usr/bin/env bash
# @generated by expo-modules-autolinking
set -eo pipefail
function with_node() {
# Start with a default
export NODE_BINARY=$(command -v node)
# Override the default with the global environment
ENV_PATH="$PODS_ROOT/../.xcode.env"
if [[ -f "$ENV_PATH" ]]; then
source "$ENV_PATH"
fi
# Override the global with the local environment
LOCAL_ENV_PATH="${ENV_PATH}.local"
if [[ -f "$LOCAL_ENV_PATH" ]]; then
source "$LOCAL_ENV_PATH"
fi
if [[ -n "$NODE_BINARY" && -x "$NODE_BINARY" ]]; then
echo "Node found at: ${NODE_BINARY}"
else
cat >&2 << NODE_NOT_FOUND
error: Could not find "node" executable while running an Xcode build script.
You need to specify the path to your Node.js executable by defining an environment variable named NODE_BINARY in your project's .xcode.env or .xcode.env.local file.
You can set this up quickly by running:
echo "export NODE_BINARY=\\$(command -v node)" >> .xcode.env
in the ios folder of your project.
NODE_NOT_FOUND
exit 1
fi
# Execute argument, if present
if [[ "$#" -gt 0 ]]; then
"$NODE_BINARY" "$@"
fi
}
with_node \\
--no-warnings \\
--eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))" \\
generate-modules-provider #{args.join(' ')} \\
--target "#{modules_provider_path}" \\
--platform "apple" \\
--packages #{package_names.join(' ')}
SUPPORT_SCRIPT
end
end # module ProjectIntegrator
end # module Expo

View File

@@ -0,0 +1,70 @@
# Copyright 2018-present 650 Industries. All rights reserved.
module Expo
class ReactImportPatcher
public def initialize(installer, options)
@root = installer.sandbox.root
@module_dirs = get_module_dirs(installer)
@options = options
end
public def run!
args = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [\'' + __dir__ + '\'] }))(process.argv.slice(1))',
'patch-react-imports',
'--pods-root',
File.expand_path(@root),
]
if @options[:dry_run]
args.append('--dry-run')
end
@module_dirs.each do |dir|
args.append(File.expand_path(dir))
end
Pod::UI.message "Executing ReactImportsPatcher node command: #{Shellwords.join(args)}"
time_begin = Process.clock_gettime(Process::CLOCK_MONOTONIC)
system(*args)
elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - time_begin
Pod::UI.info "expo_patch_react_imports! took #{elapsed_time.round(4)} seconds to transform files."
end
private def get_module_dirs(installer)
unless installer.pods_project
Pod::UI.message '`pods_project` not found. This is expected when `:incremental_installation` is enabled in your project\'s Podfile.'
return []
end
result = []
installer.pods_project.development_pods.children.each do |pod|
if pod.is_a?(Xcodeproj::Project::Object::PBXFileReference) && pod.path.end_with?('.xcodeproj')
# Support generate_multiple_pod_projects or use_frameworks!
project = Xcodeproj::Project.open(File.join(installer.sandbox.root, pod.path))
groups = project.groups.select { |group| !(['Dependencies', 'Frameworks', 'Products'].include? group.name) }
groups.each do |group|
result.append(group.real_path.to_s)
end
else
result.append(pod.real_path.to_s)
end
end
result
.select { |dir| dir.include? '/node_modules/' }
.reject do |dir|
# Exclude known dirs unnecessary to patch and reduce processing time
# Since we are using real (absolute) pathnames we need to assert that we are inside of the node_modules
# directory to not collide with other directories in the user's filesystem.
# We reject the react-native package and packages starting with expo-
dir.match(%r{^.*/node_modules/(react-native(/.*)?|expo-.*)$})
end
end
end # class ReactImportPatcher
end # module Expo

View File

@@ -0,0 +1,45 @@
require 'open3'
require 'pathname'
def generate_or_remove_xcode_env_updates_file!()
project_directory = Pod::Config.instance.project_root
xcode_env_file = File.join(project_directory, '.xcode.env.updates')
ex_updates_native_debug = ENV['EX_UPDATES_NATIVE_DEBUG'] == '1' ||
ENV['EX_UPDATES_NATIVE_DEBUG'] == 'true'
if ex_updates_native_debug
Pod::UI.info "EX_UPDATES_NATIVE_DEBUG is set; auto-generating `.xcode.env.updates` to disable packager and generate debug bundle"
if File.exist?(xcode_env_file)
File.delete(xcode_env_file)
end
File.write(xcode_env_file, <<~EOS
export FORCE_BUNDLING=1
unset SKIP_BUNDLING
export RCT_NO_LAUNCH_PACKAGER=1
EOS
)
else
if File.exist?(xcode_env_file)
Pod::UI.info "EX_UPDATES_NATIVE_DEBUG has been unset; removing `.xcode.env.updates`"
File.delete(xcode_env_file)
end
end
end
def maybe_generate_xcode_env_file!()
project_directory = Pod::Config.instance.project_root
xcode_env_file = File.join(project_directory, '.xcode.env.local')
if File.exist?(xcode_env_file)
return
end
# Adding the meta character `;` at the end of command for Ruby `Kernel.exec` to execute the command in shell.
stdout, stderr, status = Open3.capture3('node --print "process.argv[0]";')
node_path = stdout.strip
if !stderr.empty? || status.exitstatus != 0 || node_path.empty?
Pod::UI.warn "Unable to generate `.xcode.env.local` for Node.js binary path: #{stderr}"
else
Pod::UI.info "Auto-generating `.xcode.env.local` with $NODE_BINARY=#{node_path}"
File.write(xcode_env_file, "export NODE_BINARY=\"#{node_path}\"\n")
end
end