diff --git a/ios/RNMBX/RNMBXLocation.swift b/ios/RNMBX/RNMBXLocation.swift index a6d9b76e8..9e12ed440 100644 --- a/ios/RNMBX/RNMBXLocation.swift +++ b/ios/RNMBX/RNMBXLocation.swift @@ -1,3 +1,15 @@ +/*** +to: ios/rnmbx/RNMBXLocation.swift +userEditable: true +***/ + @objc(RNMBXLocation) open class RNMBXLocation : RNMBXMapComponentBase { -} + + + @objc + public static func someMethod(_ view: RNMBXLocation, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + // TODO implement + } + +} \ No newline at end of file diff --git a/package.json b/package.json index 996deed17..5b3d78597 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@sinonjs/fake-timers": "^8.0.1", "@testing-library/react-native": "^12.4.0", "@types/debounce": "^1.2.1", + "@types/ejs": "^3.1.5", "@types/mapbox-gl": "^2.7.5", "@typescript-eslint/eslint-plugin": "^5.37.0", "@typescript-eslint/parser": "^5.37.0", @@ -105,6 +106,7 @@ "eslint-plugin-jest": "^27.0.1", "expo": "^47.0.0", "expo-module-scripts": "^3.0.4", + "gray-matter": "^4.0.2", "husky": "^8.0.1", "jest": "29.7.0", "jest-cli": "29.7.0", @@ -115,17 +117,21 @@ "prettier": "2.7.1", "react": "18.2.0", "react-docgen": "rnmapbox/react-docgen#rnmapbox-dist-react-docgen-v6", - "react-native": "0.73.0-rc.4", + "react-native": "0.74.2", "react-native-builder-bob": "^0.23.1", "react-test-renderer": "18.2.0", "ts-node": "10.9.1", - "typescript": "5.1.3", - "@mdx-js/mdx": "^3.0.0" + "typescript": "5.1.3" }, "codegenConfig": { "name": "rnmapbox_maps_specs", "type": "all", "jsSrcsDir": "src/specs", + "includesGeneratedCode": true, + "outputDir": { + "android": "android/src/main/codegen", + "ios": "ios/codegen" + }, "android": { "javaPackageName": "com.rnmapbox.rnmbx" } diff --git a/ios/RNMBX/RNMBXLocationComponentView.h b/scripts/rnmbxcodegen/component/ios/component-view.h.ejs similarity index 63% rename from ios/RNMBX/RNMBXLocationComponentView.h rename to scripts/rnmbxcodegen/component/ios/component-view.h.ejs index 8972db940..61573ade9 100644 --- a/ios/RNMBX/RNMBXLocationComponentView.h +++ b/scripts/rnmbxcodegen/component/ios/component-view.h.ejs @@ -1,3 +1,6 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>ComponentView.h +***/ #ifdef RCT_NEW_ARCH_ENABLED #import @@ -7,8 +10,7 @@ NS_ASSUME_NONNULL_BEGIN - -@interface RNMBXLocationComponentView : RCTViewComponentView +@interface <%= Name %>ComponentView : RCTViewComponentView @end diff --git a/ios/RNMBX/RNMBXLocationComponentView.mm b/scripts/rnmbxcodegen/component/ios/component-view.mm.ejs similarity index 73% rename from ios/RNMBX/RNMBXLocationComponentView.mm rename to scripts/rnmbxcodegen/component/ios/component-view.mm.ejs index 4c3338f54..ff87da4eb 100644 --- a/ios/RNMBX/RNMBXLocationComponentView.mm +++ b/scripts/rnmbxcodegen/component/ios/component-view.mm.ejs @@ -1,6 +1,9 @@ +/*** +to: ios/rnmbx/generated/<%= Name %>ComponentView.mm +***/ #ifdef RCT_NEW_ARCH_ENABLED -#import "RNMBXLocationComponentView.h" +#import "<%= Name %>ComponentView.h" #import #import @@ -17,16 +20,16 @@ using namespace facebook::react; -@implementation RNMBXLocationComponentView { - RNMBXLocation *_view; +@implementation <%= Name %>ComponentView { + <%= Name %> *_view; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); + static const auto defaultProps = std::make_sharedProps>(); _props = defaultProps; - _view = [[RNMBXLocation alloc] init]; + _view = [[<%= Name %> alloc] init]; [self prepareView]; self.contentView = _view; @@ -45,7 +48,7 @@ - (void)prepareView if (strongSelf != nullptr && strongSelf->_eventEmitter != nullptr) { auto type = std::string([[event objectForKey:@"type"] UTF8String]); auto payload = convertIdToFollyDynamic([event objectForKey:@"payload"]); - RNMBXLocationEventEmitter::OnStatusChanged event = {type, payload}; + <%= Name %>EventEmitter::OnStatusChanged event = {type, payload}; strongSelf->_eventEmitter->onStatusChanged(event); } }]; @@ -56,13 +59,13 @@ - (void)prepareView + (ComponentDescriptorProvider)componentDescriptorProvider { - return concreteComponentDescriptorProvider(); + return concreteComponentDescriptorProvider<<%= Name %>ComponentDescriptor>(); } - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps { - const auto &oldViewProps = static_cast(*oldProps); - const auto &newViewProps = static_cast(*props); + const auto &oldViewProps = static_castProps &>(*oldProps); + const auto &newViewProps = static_castProps &>(*props); if (!oldProps.get() || oldViewProps.transitionsToIdleUponUserInteraction != newViewProps.transitionsToIdleUponUserInteraction) { _view.transitionsToIdleUponUserInteraction = convertDynamicToOptional_boolean(newViewProps.transitionsToIdleUponUserInteraction, @"transitionsToIdleUponUserInteraction"); @@ -76,9 +79,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } @end -Class RNMBXViewportCls(void) +Class <%= Name %>Cls(void) { - return RNMBXLocationComponentView.class; + return <%= Name %>ComponentView.class; } #endif // RCT_NEW_ARCH_ENABLED diff --git a/scripts/rnmbxcodegen/component/ios/component.swift.ejs b/scripts/rnmbxcodegen/component/ios/component.swift.ejs new file mode 100644 index 000000000..043aeba74 --- /dev/null +++ b/scripts/rnmbxcodegen/component/ios/component.swift.ejs @@ -0,0 +1,15 @@ +/*** +to: ios/rnmbx/<%= Name %>.swift +userEditable: true +***/ + +@objc(<%= Name %>) +open class <%= Name %> : RNMBXMapComponentBase { + +<% module.spec.properties.forEach(function (property) { %> + @objc + public static func <%= property.name %>(_ view: <%= ComponentName %>, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + // TODO implement + } +<% }) %> +} \ No newline at end of file diff --git a/ios/RNMBX/RNMBXLocationComponentModule.h b/scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs similarity index 66% rename from ios/RNMBX/RNMBXLocationComponentModule.h rename to scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs index 22ef7c9fd..d91979c49 100644 --- a/ios/RNMBX/RNMBXLocationComponentModule.h +++ b/scripts/rnmbxcodegen/componentmodule/ios/module.h.ejs @@ -1,3 +1,6 @@ +/*** +to: ios/RNMBX/generated/<%= Name %>.h +***/ #import #import @@ -7,9 +10,9 @@ #import #endif -@interface RNMBXLocationComponentModule : NSObject +@interface <%= Name %> : NSObject #ifdef RCT_NEW_ARCH_ENABLED - +ModuleSpec> #else #endif diff --git a/ios/RNMBX/RNMBXLocationComponentModule.mm b/scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs similarity index 57% rename from ios/RNMBX/RNMBXLocationComponentModule.mm rename to scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs index 8bb9bcaf8..98493c3d8 100644 --- a/ios/RNMBX/RNMBXLocationComponentModule.mm +++ b/scripts/rnmbxcodegen/componentmodule/ios/module.mm.ejs @@ -1,17 +1,20 @@ +/*** +to: ios/RNMBX/generated/<%= Name %>.mm +***/ #import #import #import -#import "RNMBXLocationComponentModule.h" +#import "<%= Name %>.h" + +#import "<%= ComponentName %>ComponentView.h" #ifdef RCT_NEW_ARCH_ENABLED -#import "RNMBXLocationComponentView.h" +#import "<%= ComponentName %>ComponentView.h" #endif // RCT_NEW_ARCH_ENABLED #import "rnmapbox_maps-Swift.pre.h" -@class RNMBXLocation; - -@implementation RNMBXLocationComponentModule +@implementation <%= Name %> RCT_EXPORT_MODULE(); @@ -32,39 +35,39 @@ - (dispatch_queue_t)methodQueue - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); + return std::make_sharedModuleSpecJSI>(params); } #endif // RCT_NEW_ARCH_ENABLED -- (void)withLocation:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXLocation *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName +- (void)with<%= Name %>:(nonnull NSNumber*)viewRef block:(void (^)(<%= ComponentName %> *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName { #ifdef RCT_NEW_ARCH_ENABLED [self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) { - RNMBXLocationComponentView *componentView = [self.viewRegistry_DEPRECATED viewForReactTag:viewRef]; - RNMBXLocation *view = componentView.contentView; + <%= ComponentName %>ComponentView *componentView = [self.viewRegistry_DEPRECATED viewForReactTag:viewRef]; + <%= ComponentName %> *view = componentView.contentView; #else [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNMBXLocation *view = [uiManager viewForReactTag:viewRef]; + <%= ComponentName %> *view = [uiManager viewForReactTag:viewRef]; #endif // RCT_NEW_ARCH_ENABLED if (view != nil) { - block(view); + block(view); } else { reject(methodName, [NSString stringWithFormat:@"Unknown reactTag: %@", viewRef], nil); } }]; } -#if false -RCT_EXPORT_METHOD(someMethod:(nonnull NSNumber *)viewRef +<% module.spec.properties.forEach(function (property) { %> +RCT_EXPORT_METHOD(<%= property.name %>:(nonnull NSNumber *)viewRef resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - [self withLocation:viewRef block:^(RNMBXCamera *view) { - [RNMBXLocationManager someMethod:view resolve:resolve reject:reject]; - } reject:reject methodName:@"someMethod"]; + [self with<%= Name %>:viewRef block:^(<%= ComponentName %> *view) { + [<%= ComponentName %> <%= property.name %>:view resolve:resolve reject:reject]; + } reject:reject methodName:@"<%= property.name %>"]; } -#endif +<% }) %> @end diff --git a/scripts/rnmbxcodegen/rnmbxcodegen.ts b/scripts/rnmbxcodegen/rnmbxcodegen.ts new file mode 100644 index 000000000..7bbc42e25 --- /dev/null +++ b/scripts/rnmbxcodegen/rnmbxcodegen.ts @@ -0,0 +1,190 @@ +import path from 'path'; +import fs from 'fs'; + +import ejs from 'ejs'; +import _fm from 'gray-matter'; +import type { + SchemaType, + NativeModuleSchema, + ComponentShape, +} from '@react-native/codegen/lib/CodegenSchema'; + +const ROOT_DIR = path.resolve(__dirname, '..', '..'); +const PAK_JSON_PATH = path.join(ROOT_DIR, 'package.json'); +const pak = JSON.parse(fs.readFileSync(PAK_JSON_PATH, 'utf-8')); +const SPEC_DIR = path.join(ROOT_DIR, pak.codegenConfig.jsSrcsDir); + +const SCHEMA = path.join(ROOT_DIR, 'tmp/generated/schema.json'); +const COMPONENT_TEMPLATE_ROOT = path.join(__dirname, 'component'); +const MODULE_TEMPLATE_ROOT = path.join(__dirname, 'componentmodule'); +const TEMPLATE_SUBDIRS = ['ios', 'android']; + +function fm(body: string): { body: string; attributes: T } { + const result = _fm(body, { + delimiters: ['/***', '***/'], + }); + + return { + body, + attributes: result.data as T, + }; +} + +function readRNCodegenSchema(): SchemaType { + const schema = fs.readFileSync(SCHEMA, 'utf8'); + return JSON.parse(schema); +} + +function renderTemplate( + template: string, + args: { [key: string]: string | object }, + config: { [key: string]: string | object }, + options: { filename?: string } = {}, +) { + return ejs.render(template, { ...args, ...config }, options); +} + +function generate( + templateRoomPath: string, + name: string, + config: T, + getMetadata?: (name: string) => T, +) { + const templateRoots = TEMPLATE_SUBDIRS.map((subdir) => + path.join(templateRoomPath, subdir), + ); + + templateRoots.forEach((templateRoot) => { + if (!fs.existsSync(templateRoot)) return; + + const files = fs.readdirSync(templateRoot); + files.forEach((file) => { + const content = fm<{ [key: string]: string }>( + fs.readFileSync(path.join(templateRoot, file)).toString(), + ); + const args = { Name: name }; + const { attributes } = content; + + const metadata = getMetadata ? getMetadata(name) : {}; + const actConfig = { ...config, ...metadata }; + + const renderedAttrs: { [key: string]: string } = Object.entries( + attributes, + ).reduce((obj, [key, value]) => { + if (typeof value !== 'string') return obj; + return { + ...obj, + [key]: renderTemplate(value, args, actConfig), + }; + }, {}); + + const toPath = path.join(ROOT_DIR, renderedAttrs.to); + fs.mkdirSync(path.dirname(toPath), { recursive: true }); + if (attributes.userEditable && fs.existsSync(toPath)) { + console.log('Skipping user editable file - already exists:', toPath); + } + fs.writeFileSync( + toPath, + renderTemplate(content.body, args, actConfig, { + filename: path.join(templateRoot, file), + }), + ); + }); + }); +} + +function generateCodeFromComponent( + componentName: string, + componentInfo: ComponentInfo, +) { + generate(COMPONENT_TEMPLATE_ROOT, componentName, { + ComponentName: componentName, + Name: componentName, + ...componentInfo, + }); +} + +function generateCodeFromModule(moduleName: string, moduleInfo: ModuleInfo) { + generate(MODULE_TEMPLATE_ROOT, moduleName, { + ModuleName: moduleName, + Name: moduleName, + ...moduleInfo, + }); +} + +type ComponentInfo = { + component: ComponentShape; + module?: NativeModuleSchema; + ModuleName?: string; +}; + +type ModuleInfo = { + module: NativeModuleSchema; + component?: ComponentShape; + ComponentName?: string; +}; + +const componentsToGenerate: { + [key: string]: ComponentInfo; +} = {}; + +const modulesToGenerate: { + [key: string]: ModuleInfo; +} = {}; + +Object.entries(readRNCodegenSchema().modules).forEach( + ([moduleName, module]) => { + const filename = + module.type === 'Component' + ? `${moduleName}NativeComponent.ts` + : `${moduleName}.ts`; + const moduleSpecPath = path.join(SPEC_DIR, filename); + const moduleSpecBody = fs.readFileSync(moduleSpecPath, 'utf-8'); + const matter = fm<{ rnmbxcodegen?: boolean; component?: string }>( + moduleSpecBody, + ); + + if (matter.attributes.rnmbxcodegen === true) { + if (module.type === 'Component') { + const { components } = module; + const componentsList = Object.entries(components); + if (componentsList.length > 1) { + throw new Error( + `Only one component per file is supported ${Object.keys( + components, + ).join(', ')}`, + ); + return; + } + const [[componentName, componentData]] = componentsList; + componentsToGenerate[componentName] = { component: componentData }; + } else if (module.type === 'NativeModule') { + const info: ModuleInfo = { module }; + if (matter.attributes.component) { + info.ComponentName = matter.attributes.component; + } + modulesToGenerate[module.moduleName] = info; + } + } + }, +); + +Object.entries(modulesToGenerate).forEach(([moduleName, module]) => { + if (module.ComponentName != null) { + const componentName = module.ComponentName; + const componentToGenerate = componentsToGenerate[componentName]; + module.component = componentToGenerate.component; + componentToGenerate.module = module.module; + componentToGenerate.ModuleName = moduleName; + } +}); + +Object.entries(componentsToGenerate).forEach( + ([componentName, componentInfo]) => { + generateCodeFromComponent(componentName, componentInfo); + }, +); + +Object.entries(modulesToGenerate).forEach(([moduleName, moduleInfo]) => { + generateCodeFromModule(moduleName, moduleInfo); +}); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index b29b284ec..b3467fefe 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "allowJs": true, }, - "include": ["*.ts", "codepart-replace.mjs"] + "include": ["*.ts", "codepart-replace.mjs", "*/**/*.ts"], } \ No newline at end of file diff --git a/src/specs/NativeRNMBXLocationModule.ts b/src/specs/NativeRNMBXLocationComponentModule.ts similarity index 69% rename from src/specs/NativeRNMBXLocationModule.ts rename to src/specs/NativeRNMBXLocationComponentModule.ts index a77e03114..46e858336 100644 --- a/src/specs/NativeRNMBXLocationModule.ts +++ b/src/specs/NativeRNMBXLocationComponentModule.ts @@ -1,3 +1,7 @@ +/*** +rnmbxcodegen: true +component: RNMBXLocation +***/ import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; import { Int32 } from 'react-native/Libraries/Types/CodegenTypes'; import { TurboModuleRegistry } from 'react-native'; @@ -8,4 +12,6 @@ export interface Spec extends TurboModule { someMethod(viewRef: ViewRef): Promise; } -export default TurboModuleRegistry.getEnforcing('RNMBXLocationModule'); +export default TurboModuleRegistry.getEnforcing( + 'RNMBXLocationComponentModule', +); diff --git a/src/specs/RNMBXLocationNativeComponent.ts b/src/specs/RNMBXLocationNativeComponent.ts index d028bae22..7cfcab7e2 100644 --- a/src/specs/RNMBXLocationNativeComponent.ts +++ b/src/specs/RNMBXLocationNativeComponent.ts @@ -1,3 +1,6 @@ +/*** +rnmbxcodegen: true +***/ import type { HostComponent, ViewProps } from 'react-native'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';