diff --git a/README.md b/README.md index 54dba5e..233e268 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ npm init --yes now numic ./my-starter-app app # Tempalte with navigation, data, This will prompt for an app name that can only contain **alphanumeric** characters and will be used as the initial bundle identifier. Using `NumicApp` as the name will result in `com.numicapp` as the bundle identifier. The name as well as the display name can later be configured in `app.json`. +> [!IMPORTANT] +> This project follows an alwasy up-to-date policy. Make sure to migrate to the newest React Native version when upgrading. + ## Commands This framework provides the following commands that will be added to `scripts` in `package.json` upon installation. diff --git a/configure.ts b/configure.ts index 064ed90..65e2980 100644 --- a/configure.ts +++ b/configure.ts @@ -3,6 +3,7 @@ import { join } from 'path' import { formatPackageJson } from 'pakag' import merge from 'deepmerge' import parse from 'parse-gitignore' +import { parse as parseJsonWithComments } from 'json5' import { basePath, options } from './helper' import { userGitignore, filterPluginIgnores } from './configuration/gitignore' import { packageJson } from './configuration/package' @@ -46,7 +47,7 @@ export const configureTsConfig = () => { // Make sure extended properties aren't duplicated. try { - const extendedProperties = JSON.parse(readFileSync(rnTsconfigPath, 'utf-8')) + const extendedProperties = parseJsonWithComments(readFileSync(rnTsconfigPath, 'utf-8')) // Avoid duplicate values. Object.keys(configuration.compilerOptions).forEach((key) => { @@ -60,7 +61,7 @@ export const configureTsConfig = () => { if (Array.isArray(configuration.exclude)) { configuration.exclude = configuration.exclude.filter( - (item: string) => !extendedProperties.exclude.includes(item) + (item: string) => !extendedProperties.exclude.includes(item), ) if (configuration.exclude.length === 0) { @@ -91,7 +92,7 @@ export const configureGitignore = () => { if (existsSync(gitIgnorePath)) { entries = filterPluginIgnores( - entries.concat(parse(readFileSync(gitIgnorePath, 'utf8')).patterns) + entries.concat(parse(readFileSync(gitIgnorePath, 'utf8')).patterns), ) } @@ -117,7 +118,7 @@ const configurePackageJson = async (isFirstInstall: boolean) => { // Format with prettier and sort before writing. writeFileSync( join(basePath(), './package.json'), - await formatPackageJson(JSON.stringify(generatedPackageJson)) + await formatPackageJson(JSON.stringify(generatedPackageJson)), ) options().pkg = generatedPackageJson diff --git a/documentation/RELEASE.md b/documentation/RELEASE.md new file mode 100644 index 0000000..07fa63a --- /dev/null +++ b/documentation/RELEASE.md @@ -0,0 +1,53 @@ +# Release + +## Release for Android + +> [!TIP] +> The plugin already takes care of a lot of steps in the release process. In case you run into any issues or the below documenation might be out-of-date consult the [official React Native APK release documentation](https://reactnative.dev/docs/signed-apk-android). + +To release a React Native app for Android a few manual steps are currently required. First you need JDK (not just the JRE bundled with Android Studio) downloaded and installed from [Oracle Downloads](https://www.oracle.com/java/technologies/downloads) and linked in `~/.zshrc` with `export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-[DOWNLOADED_VERSION].jdk/Contents/Home/bin/java`, apply changes with `source ~/.zshrc`. Once that's done generate a signing key: + +```sh +sudo keytool -genkey -v -keystore my-upload-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000 +``` + +Add the generated file to `/android/app/my-upload-key.keystore`, no need to commit this file as it will be integrated into the patch (it's fairly short). Once that's also done add the password you just entered as well as the file location to the end of `/android/gradle.properties`: + +```sh + +MYAPP_UPLOAD_STORE_FILE=my-upload-key.keystore +MYAPP_UPLOAD_KEY_ALIAS=my-key-alias +MYAPP_UPLOAD_STORE_PASSWORD=***** +MYAPP_UPLOAD_KEY_PASSWORD=***** +``` + +The make the following changes to `/android/app/build.gradle`: + +```sh + keyAlias 'androiddebugkey' + keyPassword 'android' + } ++ release { ++ if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) { ++ storeFile file(MYAPP_UPLOAD_STORE_FILE) ++ storePassword MYAPP_UPLOAD_STORE_PASSWORD ++ keyAlias MYAPP_UPLOAD_KEY_ALIAS ++ keyPassword MYAPP_UPLOAD_KEY_PASSWORD ++ } ++ } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { +- // Caution! In production, you need to generate your own keystore file. +- // see https://reactnative.dev/docs/signed-apk-android. +- signingConfig signingConfigs.debug ++ signingConfig signingConfigs.release + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } +``` + +This will read the keyfile when running the `Distribute` script and build with the release configuration. Don't forget to revert `JAVA_HOME` to the JRE location in Android Studio. diff --git a/package.json b/package.json index f7897e4..854ae11 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "padua build", "postinstall": "skip-local-postinstall dist/installation.js", "start": "padua watch", - "test": "vitest run --no-threads --dir test" + "test": "vitest run --pool=threads --poolOptions.threads.singleThread --dir test" }, "padua": { "entry": [ @@ -33,26 +33,27 @@ "command-exists": "^1.2.9", "deepmerge": "^4.3.1", "eslint-plugin-prettier": "^5.0.1", - "fast-glob": "^3.3.1", - "global-cache-dir": "^5.0.0", + "fast-glob": "^3.3.2", + "global-cache-dir": "^6.0.0", "is-ci": "^3.0.1", - "logua": "^3.0.2", + "json5": "^2.2.3", + "logua": "^3.0.3", "pakag": "^3.1.1", "parse-gitignore": "^2.0.0", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "prompts": "^2.4.2", "semver": "^7.5.4", "semver-sort": "^1.0.0", "skip-local-postinstall": "^2.0.4" }, "devDependencies": { - "@types/command-exists": "^1.2.2", - "@types/prompts": "^2.4.7", - "@types/semver": "^7.5.4", + "@types/command-exists": "^1.2.3", + "@types/prompts": "^2.4.9", + "@types/semver": "^7.5.6", "jest-fixture": "^4.1.0", - "padua": "^2.0.6", - "react-native": "^0.72.6", - "vitest": "^0.34.6" + "padua": "^2.0.7", + "react-native": "^0.73.0", + "vitest": "^1.0.2" }, "peerDependencies": { "react-native": ">= 0.69" diff --git a/plugin/bundle-id.ts b/plugin/bundle-id.ts index 5ae4372..22d2ce7 100644 --- a/plugin/bundle-id.ts +++ b/plugin/bundle-id.ts @@ -7,7 +7,7 @@ const searchForFileAndReplace = ( filePathGlob: string | string[], matcher: RegExp, replacement: string, - nativePath: string + nativePath: string, ) => { const files = glob.sync(filePathGlob, { cwd: nativePath, @@ -57,7 +57,7 @@ export default async ({ const cleanVersion = semver.coerce(version).version if (!cleanVersion || !semver.valid(cleanVersion) || !semver.gte(cleanVersion, '0.71.0')) { log( - `bundleId can only be customized with React Native >= 0.71 while the current version is "${cleanVersion}"` + `bundleId can only be customized with React Native >= 0.71 while the current version is "${cleanVersion}"`, ) return } @@ -67,31 +67,30 @@ export default async ({ appBuildGradleContents = appBuildGradleContents.replaceAll( /namespace\s"[\w.]+"/g, - `namespace "${bundleId}"` + `namespace "${bundleId}"`, ) appBuildGradleContents = appBuildGradleContents.replaceAll( /applicationId\s"[\w.]+"/g, - `applicationId "${bundleId}"` + `applicationId "${bundleId}"`, ) writeFileSync(appBuildGradleFilePath, appBuildGradleContents) searchForFileAndReplace( [ - 'android/app/src/*/java/com/*/ReactNativeFlipper.java', - 'android/app/src/main/java/com/*/MainActivity.java', - 'android/app/src/main/java/com/*/MainApplication.java', + 'android/app/src/main/java/com/*/MainActivity.kt', + 'android/app/src/main/java/com/*/MainApplication.kt', ], - /package\s[\w.]+;/g, - `package ${bundleId};`, - nativePath + /package\s[\w.]+/g, + `package ${bundleId}`, + nativePath, ) searchForFileAndReplace( 'ios/*.xcodeproj/project.pbxproj', /PRODUCT_BUNDLE_IDENTIFIER = "org\.reactjs\.native\.example\.\$\(PRODUCT_NAME:rfc1034identifier\)";/g, `PRODUCT_BUNDLE_IDENTIFIER = ${bundleId};`, - nativePath + nativePath, ) } diff --git a/template-cache.ts b/template-cache.ts index dec1401..2e3150f 100644 --- a/template-cache.ts +++ b/template-cache.ts @@ -41,9 +41,10 @@ export const cacheTemplate = (nativeOptions: NativeOptions) => { // TODO necessary? writeFileSync(join(folders.numic, 'package.json'), '{ "name": "numic-native" }') // DOC https://github.com/react-native-community/cli/blob/master/packages/cli/src/commands/init/index.ts + // TODO --skip-git-init not yet implemented try { execSync( - `npx react-native init ${nativeOptions.appName} --skip-install --version ${nativeOptions.version}`, + `npx react-native init ${nativeOptions.appName} --skip-install --install-pods false --version ${nativeOptions.version}`, { cwd: directory, encoding: 'utf8', diff --git a/template/app/metro.config.cjs b/template/app/metro.config.cjs new file mode 100644 index 0000000..0b7478a --- /dev/null +++ b/template/app/metro.config.cjs @@ -0,0 +1,15 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config') + +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('metro-config').MetroConfig} + */ +const config = { + resolver: { + unstable_enablePackageExports: true, + }, +} + +module.exports = mergeConfig(getDefaultConfig(__dirname), config) diff --git a/template/app/package.json b/template/app/package.json index 4ba9fc2..b00aa60 100644 --- a/template/app/package.json +++ b/template/app/package.json @@ -14,33 +14,33 @@ } }, "dependencies": { - "epic-language": "^0.4.0", - "mobx": "^6.10.2", + "epic-language": "^0.5.0", + "mobx": "^6.12.0", "mobx-react-lite": "^4.0.5", "react": "^18.2.0", - "react-native": "^0.72.6", + "react-native": "^0.73.0", "reactigation": "^4.0.1", "responsive-react-native": "^1.0.1" }, "devDependencies": { "@react-native/eslint-config": "^0.74.0", "@testing-library/jest-native": "^5.4.3", - "@testing-library/react-native": "^12.3.0", + "@testing-library/react-native": "^12.4.1", "@tsconfig/react-native": "^3.0.2", - "@types/jest": "^29.5.6", - "@types/react-native": "^0.72.5", - "@typescript-eslint/eslint-plugin": "^6.8.0", - "@typescript-eslint/parser": "^6.8.0", + "@types/jest": "^29.5.11", + "@types/react-native": "^0.72.8", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^6.13.2", "android-sdk-numic-plugin": "^1.0.3", "babel-jest": "^29.7.0", - "eslint": "^8.52.0", + "eslint": "^8.55.0", "eslint-plugin-prettier": "^5.0.1", "icon-numic-plugin": "^1.4.3", "jest": "^29.7.0", "metro-react-native-babel-preset": "^0.77.0", "numic": "latest", "react-test-renderer": "^18.2.0", - "typescript": "^5.2.2" + "typescript": "^5.3.3" }, "overrides": { "chalk": "^4.1.2" diff --git a/template/default/metro.config.cjs b/template/default/metro.config.cjs new file mode 100644 index 0000000..0b7478a --- /dev/null +++ b/template/default/metro.config.cjs @@ -0,0 +1,15 @@ +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config') + +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('metro-config').MetroConfig} + */ +const config = { + resolver: { + unstable_enablePackageExports: true, + }, +} + +module.exports = mergeConfig(getDefaultConfig(__dirname), config) diff --git a/template/default/package.json b/template/default/package.json index b44a774..ad95033 100644 --- a/template/default/package.json +++ b/template/default/package.json @@ -15,27 +15,27 @@ }, "dependencies": { "react": "^18.2.0", - "react-native": "^0.72.6" + "react-native": "^0.73.0" }, "type": "module", "devDependencies": { "@react-native/eslint-config": "^0.74.0", "@tsconfig/react-native": "^3.0.2", - "@types/jest": "^29.5.6", - "@types/react-native": "^0.72.5", - "@types/react-test-renderer": "^18.0.5", - "@typescript-eslint/eslint-plugin": "^6.8.0", - "@typescript-eslint/parser": "^6.8.0", + "@types/jest": "^29.5.11", + "@types/react-native": "^0.72.8", + "@types/react-test-renderer": "^18.0.7", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^6.13.2", "android-sdk-numic-plugin": "^1.0.3", "babel-jest": "^29.7.0", - "eslint": "^8.52.0", + "eslint": "^8.55.0", "eslint-plugin-prettier": "^5.0.1", "icon-numic-plugin": "^1.4.3", "jest": "^29.7.0", "metro-react-native-babel-preset": "^0.77.0", "numic": "latest", "react-test-renderer": "18.2.0", - "typescript": "^5.2.2" + "typescript": "^5.3.3" }, "metro": {}, "jest": { diff --git a/test/native.test.ts b/test/native.test.ts index cc2a2d1..f77dbe5 100644 --- a/test/native.test.ts +++ b/test/native.test.ts @@ -72,8 +72,8 @@ test('Creates patch for simple change in android and ios user folder.', async () const podfileContents = readFile('ios/Podfile') const changedPodfileContents = podfileContents.replace( - ':hermes_enabled => flags[:hermes_enabled],', - ':something_else_enabled => true,' + 'config = use_native_modules!', + 'config = use_active_modules' ) writeFile('ios/Podfile', changedPodfileContents) @@ -86,8 +86,8 @@ test('Creates patch for simple change in android and ios user folder.', async () expect(patchContents).toContain('mavenCentral()') expect(patchContents).toContain('navenUI()') - expect(patchContents).toContain('- :hermes_enabled => flags[:hermes_enabled],') - expect(patchContents).toContain('+ :something_else_enabled => true,') + expect(patchContents).toContain('- config = use_native_modules!') + expect(patchContents).toContain('+ config = use_active_modules') // Restore initial native folder change. writeFile('android/build.gradle', buildGradleContents) @@ -95,7 +95,7 @@ test('Creates patch for simple change in android and ios user folder.', async () expect(readFile('android/build.gradle')).not.toContain('navenUI()') expect(readFile('android/build.gradle')).toContain('mavenCentral()') - expect(readFile('ios/Podfile')).not.toContain(':something_else_enabled => true,') + expect(readFile('ios/Podfile')).not.toContain('config = use_active_modules') apply({}) @@ -105,8 +105,8 @@ test('Creates patch for simple change in android and ios user folder.', async () expect(patchedBuildGradleContents).not.toContain('mavenCentral()') expect(patchedBuildGradleContents).toContain('navenUI()') - expect(patchedPodfileContents).not.toContain(':hermes_enabled => flags[:hermes_enabled],') - expect(patchedPodfileContents).toContain(':something_else_enabled => true,') + expect(patchedPodfileContents).not.toContain('config = use_native_modules!') + expect(patchedPodfileContents).toContain('config = use_active_modules') }) test('Patches nested changes as well as file additions, renames and removals.', async () => { diff --git a/test/plugin.test.ts b/test/plugin.test.ts index c240a84..718a4f9 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -23,7 +23,7 @@ environment('plugin') const reactNativePkg = file( 'node_modules/react-native/package.json', - `{ "version": "${readFile('package.json').devDependencies['react-native'].replace('^', '')}" }` + `{ "version": "${readFile('package.json').devDependencies['react-native'].replace('^', '')}" }`, ) test('Simple plugin modifies native files.', async () => { @@ -38,7 +38,7 @@ test('Simple plugin modifies native files.', async () => { join(initialCwd, 'node_modules/simple-numic-plugin'), { recursive: true, - } + }, ) await native() @@ -62,7 +62,7 @@ test('Asynchronous plugin modifies native files.', async () => { join(initialCwd, 'node_modules/asynchronous-numic-plugin'), { recursive: true, - } + }, ) await native() @@ -115,7 +115,7 @@ test('Npm plugin can be configured.', async () => { join(initialCwd, 'node_modules/simple-numic-plugin'), { recursive: true, - } + }, ) await native() @@ -147,7 +147,7 @@ test("Plugin changes are staged and don't cause patch even after initial reposit join(initialCwd, 'node_modules/simple-numic-plugin'), { recursive: true, - } + }, ) // Same commands as iOS and Android commands would run. @@ -250,27 +250,15 @@ test('Bundle ID will be adapted when configured.', async () => { expect(appBuildGradleContents).toContain('namespace "com.tobua.numic"') expect(appBuildGradleContents).toContain('applicationId "com.tobua.numic"') - const flipperFileContents = readFile( - 'android/app/src/debug/java/com/numicapp/ReactNativeFlipper.java' - ) - - expect(flipperFileContents).toContain('package com.tobua.numic;') - - const mainActivityContents = readFile('android/app/src/main/java/com/numicapp/MainActivity.java') + const mainActivityContents = readFile('android/app/src/main/java/com/numicapp/MainActivity.kt') - expect(mainActivityContents).toContain('package com.tobua.numic;') + expect(mainActivityContents).toContain('package com.tobua.numic') const mainApplicationContents = readFile( - 'android/app/src/main/java/com/numicapp/MainApplication.java' - ) - - expect(mainApplicationContents).toContain('package com.tobua.numic;') - - const flipperReleaseFileContents = readFile( - 'android/app/src/release/java/com/numicapp/ReactNativeFlipper.java' + 'android/app/src/main/java/com/numicapp/MainApplication.kt', ) - expect(flipperReleaseFileContents).toContain('package com.tobua.numic;') + expect(mainApplicationContents).toContain('package com.tobua.numic') const iosProject = readFile('ios/NumicApp.xcodeproj/project.pbxproj') diff --git a/website/package.json b/website/package.json index 907f3a9..b295f74 100644 --- a/website/package.json +++ b/website/package.json @@ -9,33 +9,33 @@ "icon": "../logo.png" }, "dependencies": { - "@react-three/drei": "^9.88.0", + "@react-three/drei": "^9.90.1", "@react-three/eslint-plugin": "^0.1.1", - "@react-three/fiber": "^8.14.5", - "@react-three/postprocessing": "^2.15.1", - "@react-three/rapier": "^1.1.1", - "@types/node": "^20.8.4", - "@types/react": "^18.2.28", - "@types/react-dom": "^18.2.13", - "@types/three": "^0.157.0", + "@react-three/fiber": "^8.15.12", + "@react-three/postprocessing": "^2.15.11", + "@react-three/rapier": "^1.2.0", + "@types/node": "^20.10.4", + "@types/react": "^18.2.42", + "@types/react-dom": "^18.2.17", + "@types/three": "^0.159.0", "maath": "^0.10.4", - "papua": "^5.8.0", + "papua": "^5.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "three": "^0.157.0", - "typescript": "^5.2.2", - "valtio": "^1.11.2" + "three": "^0.159.0", + "typescript": "^5.3.3", + "valtio": "^1.12.1" }, "type": "module", "prettier": "papua/configuration/.prettierrc.json", "eslintConfig": { + "plugins": [ + "@react-three" + ], "extends": [ "./node_modules/papua/configuration/eslint.cjs", "plugin:@react-three/recommended" ], - "plugins": [ - "@react-three" - ], "root": true }, "stylelint": {