From 2f2d1e0bfa5e3e41bba653de2393e9a2c169bd38 Mon Sep 17 00:00:00 2001 From: Jason Cassidy <47318351+jcassidyav@users.noreply.github.com> Date: Wed, 3 Jul 2024 17:47:09 +0100 Subject: [PATCH] feat: native add command to add native source files to the project (#5806) --- .../configuration/native/native-add-java.md | 32 ++ .../configuration/native/native-add-kotlin.md | 34 ++ .../native/native-add-objective-c.md | 34 ++ .../configuration/native/native-add-swift.md | 32 ++ .../configuration/native/native-add.md | 31 ++ .../project/configuration/native/native.md | 31 ++ lib/bootstrap.ts | 11 +- lib/commands/native-add.ts | 388 ++++++++++++++++++ 8 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 docs/man_pages/project/configuration/native/native-add-java.md create mode 100644 docs/man_pages/project/configuration/native/native-add-kotlin.md create mode 100644 docs/man_pages/project/configuration/native/native-add-objective-c.md create mode 100644 docs/man_pages/project/configuration/native/native-add-swift.md create mode 100644 docs/man_pages/project/configuration/native/native-add.md create mode 100644 docs/man_pages/project/configuration/native/native.md create mode 100644 lib/commands/native-add.ts diff --git a/docs/man_pages/project/configuration/native/native-add-java.md b/docs/man_pages/project/configuration/native/native-add-java.md new file mode 100644 index 0000000000..d0f2cfa891 --- /dev/null +++ b/docs/man_pages/project/configuration/native/native-add-java.md @@ -0,0 +1,32 @@ +<% if (isJekyll) { %>--- +title: ns native add java +position: 2 +---<% } %> + +# ns native add java + +### Description + +Adds a newly generated Java file, which includes a class with the specified name, placing it in the appropriate directory. + +### Commands + +Usage | Synopsis +------|------- +Java | `$ ns native add java ` + +### Arguments + +* `` is the fully qualified name of the `Class` to create, e.g. `org.nativescript.SomeClass` + +<% if(isHtml) { %> + +### Related Commands + +Command | Description +----------|---------- +[native add swift](native-add-swift.html) | Generates and adds a Swift file containing a class of the given name. +[native add objective-c](native-add-objective-c.html) | Generates and adds Objective-C files containing an interface of the given name. +[native add java](native-add-java.html) | Generates and adds a Java file containing a class of the given name. +[native add kotlin](native-add-kotlin.html) | Generates and adds a Kotlin file containing a class of the given name. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/configuration/native/native-add-kotlin.md b/docs/man_pages/project/configuration/native/native-add-kotlin.md new file mode 100644 index 0000000000..842ac1b247 --- /dev/null +++ b/docs/man_pages/project/configuration/native/native-add-kotlin.md @@ -0,0 +1,34 @@ +<% if (isJekyll) { %>--- +title: ns native add kotlin +position: 3 +---<% } %> + +# ns native add kotlin + +### Description + +Adds a newly generated Kotlin file, which includes a class with the specified name, placing it in the appropriate directory. + +Kotlin usage requires that the `useKotlin` property is set in `gradle.properties`, the command will set this to `true`. + +### Commands + +Usage | Synopsis +------|------- +Kotlin | `$ ns native add kotlin ` + +### Arguments + +* `` is the fully qualified name of the `Class` to create, e.g. `org.nativescript.SomeClass` + +<% if(isHtml) { %> + +### Related Commands + +Command | Description +----------|---------- +[native add swift](native-add-swift.html) | Generates and adds a Swift file containing a class of the given name. +[native add objective-c](native-add-objective-c.html) | Generates and adds Objective-C files containing an interface of the given name. +[native add java](native-add-java.html) | Generates and adds a Java file containing a class of the given name. +[native add kotlin](native-add-kotlin.html) | Generates and adds a Kotlin file containing a class of the given name. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/configuration/native/native-add-objective-c.md b/docs/man_pages/project/configuration/native/native-add-objective-c.md new file mode 100644 index 0000000000..9da8111e82 --- /dev/null +++ b/docs/man_pages/project/configuration/native/native-add-objective-c.md @@ -0,0 +1,34 @@ +<% if (isJekyll) { %>--- +title: ns native add objective-c +position: 4 +---<% } %> + +# ns native add objective-c + +### Description + +Adds newly generated Objective-C files, which include an interface with the specified name, placing them in the appropriate directory. + +Objective-C usage requires that the `module.modulemap` is modified to include the header file, the command will set this entry. + +### Commands + +Usage | Synopsis +------|------- +Objective-C | `$ ns native add objective-c ` + +### Arguments + +* `` is the name of the `interface` to create, e.g. `SomeInterface` + +<% if(isHtml) { %> + +### Related Commands + +Command | Description +----------|---------- +[native add swift](native-add-swift.html) | Generates and adds a Swift file containing a class of the given name. +[native add objective-c](native-add-objective-c.html) | Generates and adds Objective-C files containing an interface of the given name. +[native add java](native-add-java.html) | Generates and adds a Java file containing a class of the given name. +[native add kotlin](native-add-kotlin.html) | Generates and adds a Kotlin file containing a class of the given name. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/configuration/native/native-add-swift.md b/docs/man_pages/project/configuration/native/native-add-swift.md new file mode 100644 index 0000000000..c89b1b1096 --- /dev/null +++ b/docs/man_pages/project/configuration/native/native-add-swift.md @@ -0,0 +1,32 @@ +<% if (isJekyll) { %>--- +title: ns native add swift +position: 5 +---<% } %> + +# ns native add swift + +### Description + +Adds a newly generated Swift file, which includes a class with the specified name, placing it in the appropriate directory. + +### Commands + +Usage | Synopsis +------|------- +Swift | `$ ns native add swift ` + +### Arguments + +* `` is the name of the `Class` to create, e.g. `SomeClass` + +<% if(isHtml) { %> + +### Related Commands + +Command | Description +----------|---------- +[native add swift](native-add-swift.html) | Generates and adds a Swift file containing a class of the given name. +[native add objective-c](native-add-objective-c.html) | Generates and adds Objective-C files containing an interface of the given name. +[native add java](native-add-java.html) | Generates and adds a Java file containing a class of the given name. +[native add kotlin](native-add-kotlin.html) | Generates and adds a Kotlin file containing a class of the given name. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/configuration/native/native-add.md b/docs/man_pages/project/configuration/native/native-add.md new file mode 100644 index 0000000000..9739442896 --- /dev/null +++ b/docs/man_pages/project/configuration/native/native-add.md @@ -0,0 +1,31 @@ +<% if (isJekyll) { %>--- +title: ns native add +position: 1 +---<% } %> + +# ns native add + +### Description + +Commands to add native files to the application placing them in the correct directory. + +### Commands + +Usage | Synopsis +------|------- +Swift | `$ ns native add swift ` +Objective-C | `$ ns native add objective-c ` +Java | `$ ns native add java ` +Kotlin | `$ ns native add kotlin ` + +<% if(isHtml) { %> + +### Related Commands + +Command | Description +----------|---------- +[native add swift](native-add-swift.html) | Generates and adds a Swift file containing a class of the given name. +[native add objective-c](native-add-objective-c.html) | Generates and adds Objective-C files containing an interface of the given name. +[native add java](native-add-java.html) | Generates and adds a Java file containing a class of the given name. +[native add kotlin](native-add-kotlin.html) | Generates and adds a Kotlin file containing a class of the given name. +<% } %> \ No newline at end of file diff --git a/docs/man_pages/project/configuration/native/native.md b/docs/man_pages/project/configuration/native/native.md new file mode 100644 index 0000000000..1399157992 --- /dev/null +++ b/docs/man_pages/project/configuration/native/native.md @@ -0,0 +1,31 @@ +<% if (isJekyll) { %>--- +title: ns native +position: 1 +---<% } %> + +# ns native + +### Description + +Commands to add native files to the application placing them in the correct directory. + +### Commands + +Usage | Synopsis +------|------- +Swift | `$ ns native add swift ` +Objective-C | `$ ns native add objective-c ` +Java | `$ ns native add java ` +Kotlin | `$ ns native add kotlin ` + +<% if(isHtml) { %> + +### Related Commands + +Command | Description +----------|---------- +[native add swift](native-add-swift.html) | Generates and adds a Swift file containing a class of the given name. +[native add objective-c](native-add-objective-c.html) | Generates and adds Objective-C files containing an interface of the given name. +[native add java](native-add-java.html) | Generates and adds a Java file containing a class of the given name. +[native add kotlin](native-add-kotlin.html) | Generates and adds a Kotlin file containing a class of the given name. +<% } %> \ No newline at end of file diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index bb99bb0650..ab4d408dfd 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -458,5 +458,14 @@ injector.require("keyCommandHelper", "./helpers/key-command-helper"); injector.requireCommand("start", "./commands/start"); injector.require("startService", "./services/start-service"); - +injector.requireCommand( + [ + "native|add", + "native|add|java", + "native|add|kotlin", + "native|add|swift", + "native|add|objective-c", + ], + "./commands/native-add" +); require("./key-commands/bootstrap"); diff --git a/lib/commands/native-add.ts b/lib/commands/native-add.ts new file mode 100644 index 0000000000..45d8c192c4 --- /dev/null +++ b/lib/commands/native-add.ts @@ -0,0 +1,388 @@ +import { IProjectData } from "../definitions/project"; +import * as fs from "fs"; +import { ICommandParameter, ICommand } from "../common/definitions/commands"; +import { IErrors } from "../common/declarations"; +import * as path from "path"; +import { injector } from "../common/yok"; +import { EOL } from "os"; + +export class NativeAddCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + protected $projectData: IProjectData, + protected $logger: ILogger, + protected $errors: IErrors + ) { + this.$projectData.initializeProjectData(); + } + + public async execute(args: string[]): Promise { + this.failWithUsage(); + + return Promise.resolve(); + } + + protected failWithUsage(): void { + this.$errors.failWithHelp( + "Usage: ns native add [swift|objective-c|java|kotlin] [class name]" + ); + } + public async canExecute(args: string[]): Promise { + this.failWithUsage(); + return false; + } + + protected getIosSourcePathBase() { + const resources = this.$projectData.getAppResourcesDirectoryPath(); + return path.join(resources, "iOS", "src"); + } + + protected getAndroidSourcePathBase() { + const resources = this.$projectData.getAppResourcesDirectoryPath(); + return path.join(resources, "Android", "src", "main", "java"); + } +} +export class NativeAddSingleCommand extends NativeAddCommand { + constructor($projectData: IProjectData, $logger: ILogger, $errors: IErrors) { + super($projectData, $logger, $errors); + } + public async canExecute(args: string[]): Promise { + if (!args || args.length !== 1) { + this.failWithUsage(); + } + + return true; + } +} + +export class NativeAddAndroidCommand extends NativeAddSingleCommand { + constructor($projectData: IProjectData, $logger: ILogger, $errors: IErrors) { + super($projectData, $logger, $errors); + } + + private getPackageName(className: string): string { + const lastDotIndex = className.lastIndexOf("."); + if (lastDotIndex !== -1) { + return className.substring(0, lastDotIndex); + } + return ""; + } + + private getClassSimpleName(className: string): string { + const lastDotIndex = className.lastIndexOf("."); + if (lastDotIndex !== -1) { + return className.substring(lastDotIndex + 1); + } + return className; + } + + private generateJavaClassContent( + packageName: string, + classSimpleName: string + ): string { + return ( + (packageName.length > 0 ? `package ${packageName};` : "") + + ` +import android.util.Log; + +public class ${classSimpleName} { + public void logMessage() { + Log.d("JS", "Hello from ${classSimpleName}!"); + } +} +` + ); + } + + private generateKotlinClassContent( + packageName: string, + classSimpleName: string + ): string { + return ( + (packageName.length > 0 ? `package ${packageName};` : "") + + ` + +import android.util.Log + +class ${classSimpleName} { + fun logMessage() { + Log.d("JS", "Hello from ${classSimpleName}!") + } +} +` + ); + } + public doJavaKotlin(className: string, extension: string): void { + const fileExt = extension == "java" ? extension : "kt"; + const packageName = this.getPackageName(className); + const classSimpleName = this.getClassSimpleName(className); + const packagePath = path.join( + this.getAndroidSourcePathBase(), + ...packageName.split(".") + ); + const filePath = path.join(packagePath, `${classSimpleName}.${fileExt}`); + + if (fs.existsSync(filePath)) { + this.$errors.failWithHelp( + `${extension} file '${filePath}' already exists.` + ); + return; + } + + if (extension == "kotlin" && !this.checkAndUpdateGradleProperties()) { + return; + } + + const fileContent = + extension == "java" + ? this.generateJavaClassContent(packageName, classSimpleName) + : this.generateKotlinClassContent(packageName, classSimpleName); + + fs.mkdirSync(packagePath, { recursive: true }); + fs.writeFileSync(filePath, fileContent); + this.$logger.info( + `${extension} file '${filePath}' generated successfully.` + ); + } + + private checkAndUpdateGradleProperties(): boolean { + const resources = this.$projectData.getAppResourcesDirectoryPath(); + + const filePath = path.join(resources, "Android", "gradle.properties"); + + if (fs.existsSync(filePath)) { + const fileContent = fs.readFileSync(filePath, "utf8"); + const propertyRegex = /^useKotlin\s*=\s*(true|false)$/m; + const match = propertyRegex.exec(fileContent); + + if (match) { + const useKotlin = match[1]; + + if (useKotlin === "false") { + this.$errors.failWithHelp( + "The useKotlin property is set to false. Stopping processing." + ); + return false; + } + + if (useKotlin === "true") { + this.$logger.warn( + 'gradle.properties already contains "useKotlin=true".' + ); + return true; + } + } else { + fs.appendFileSync(filePath, `${EOL}useKotlin=true${EOL}`); + this.$logger.info( + 'Added "useKotlin=true" property to gradle.properties.' + ); + } + } else { + fs.writeFileSync(filePath, `useKotlin=true${EOL}`); + this.$logger.info( + 'Created gradle.properties with "useKotlin=true" property.' + ); + } + return true; + } +} + +export class NativeAddJavaCommand extends NativeAddAndroidCommand { + constructor($projectData: IProjectData, $logger: ILogger, $errors: IErrors) { + super($projectData, $logger, $errors); + } + + public async execute(args: string[]): Promise { + this.doJavaKotlin(args[0], "java"); + + return Promise.resolve(); + } +} + +export class NativeAddKotlinCommand extends NativeAddAndroidCommand { + constructor($projectData: IProjectData, $logger: ILogger, $errors: IErrors) { + super($projectData, $logger, $errors); + } + + public async execute(args: string[]): Promise { + this.doJavaKotlin(args[0], "kotlin"); + + return Promise.resolve(); + } +} + +export class NativeAddObjectiveCCommand extends NativeAddSingleCommand { + constructor($projectData: IProjectData, $logger: ILogger, $errors: IErrors) { + super($projectData, $logger, $errors); + } + + public async execute(args: string[]): Promise { + this.doObjectiveC(args[0]); + + return Promise.resolve(); + } + private doObjectiveC(className: string) { + const iosSourceBase = this.getIosSourcePathBase(); + + const classFilePath = path.join(iosSourceBase, `${className}.m`); + const headerFilePath = path.join(iosSourceBase, `${className}.h`); + + if ( + this.generateObjectiveCFiles(className, classFilePath, headerFilePath) + ) { + // Modify/Generate moduleMap + this.generateOrUpdateModuleMap( + `${className}.h`, + path.join(iosSourceBase, "module.modulemap") + ); + } + } + + private generateOrUpdateModuleMap( + headerFileName: string, + moduleMapPath: string + ): void { + const moduleName = "LocalModule"; + const headerPath = headerFileName; + + let moduleMapContent = ""; + + if (fs.existsSync(moduleMapPath)) { + moduleMapContent = fs.readFileSync(moduleMapPath, "utf8"); + } + + const headerDeclaration = `header "${headerPath}"`; + + if (moduleMapContent.includes(`module ${moduleName}`)) { + // Module declaration already exists in the module map + if (moduleMapContent.includes(headerDeclaration)) { + // Header is already present in the module map + this.$logger.warn( + `Header '${headerFileName}' is already added to the module map.` + ); + return; + } + + const updatedModuleMapContent = moduleMapContent.replace( + new RegExp(`module ${moduleName} {\\s*([^}]*)\\s*}`, "s"), + `module ${moduleName} {${EOL} $1${EOL} ${headerDeclaration}${EOL}}` + ); + + fs.writeFileSync(moduleMapPath, updatedModuleMapContent); + } else { + // Module declaration does not exist in the module map + const moduleDeclaration = `module ${moduleName} {${EOL} ${headerDeclaration}${EOL} export *${EOL}}`; + + moduleMapContent += `${EOL}${EOL}${moduleDeclaration}`; + fs.writeFileSync(moduleMapPath, moduleMapContent); + } + + this.$logger.info( + `Module map '${moduleMapPath}' has been updated with the header '${headerFileName}'.` + ); + } + + private generateObjectiveCFiles( + className: string, + classFilePath: string, + interfaceFilePath: string + ): boolean { + if (fs.existsSync(classFilePath)) { + this.$errors.failWithHelp( + `Error: File '${classFilePath}' already exists.` + ); + return false; + } + + if (fs.existsSync(interfaceFilePath)) { + this.$errors.failWithHelp( + `Error: File '${interfaceFilePath}' already exists.` + ); + return false; + } + + const interfaceContent = `#import + +@interface ${className} : NSObject + +- (void)logMessage; + +@end +`; + + const classContent = `#import "${className}.h" + +@implementation ${className} + +- (void)logMessage { + NSLog(@"Hello from ${className} class!"); +} + +@end +`; + + fs.writeFileSync(classFilePath, classContent); + this.$logger.trace( + `Objective-C class file '${classFilePath}' generated successfully.` + ); + + fs.writeFileSync(interfaceFilePath, interfaceContent); + this.$logger.trace( + `Objective-C interface file '${interfaceFilePath}' generated successfully.` + ); + return true; + } +} + +export class NativeAddSwiftCommand extends NativeAddSingleCommand { + constructor($projectData: IProjectData, $logger: ILogger, $errors: IErrors) { + super($projectData, $logger, $errors); + } + + public async execute(args: string[]): Promise { + this.doSwift(args[0]); + + return Promise.resolve(); + } + + private doSwift(className: string) { + const iosSourceBase = this.getIosSourcePathBase(); + const swiftFilePath = path.join(iosSourceBase, `${className}.swift`); + this.generateSwiftFile(className, swiftFilePath); + } + + private generateSwiftFile(className: string, filePath: string): void { + const directory = path.dirname(filePath); + + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + this.$logger.trace(`Created directory: '${directory}'.`); + } + + if (fs.existsSync(filePath)) { + this.$errors.failWithHelp(`Error: File '${filePath}' already exists.`); + return; + } + + const content = `import Foundation; +import os; +@objc class ${className}: NSObject { + @objc func logMessage() { + os_log("Hello from ${className} class!") + } +}`; + + fs.writeFileSync(filePath, content); + this.$logger.info(`Swift file '${filePath}' generated successfully.`); + } +} + +injector.registerCommand(["native|add"], NativeAddCommand); +injector.registerCommand(["native|add|java"], NativeAddJavaCommand); +injector.registerCommand(["native|add|kotlin"], NativeAddKotlinCommand); +injector.registerCommand(["native|add|swift"], NativeAddSwiftCommand); +injector.registerCommand( + ["native|add|objective-c"], + NativeAddObjectiveCCommand +);