From 79fbfee5fb673dac6c17acc62323d3cffc15155f Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Fri, 14 Aug 2026 23:04:09 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20build=20standalone=20=E2=80=94=20five=20?= =?UTF-8?q?things=20the=20monorepo=20was=20quietly=20providing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The extraction typechecked from day one and had never been built. It took five attempts, each failing further along, and every cause was the same shape: something the shared node_modules or the monorepo's layout supplied without anyone declaring it. 1. The config plugin had the monorepo's geometry written into it — ../../../packages/officer-net/ios/libs, correct from apps//ios and meaningless from a repo root. It reported a path belonging to neither repo. Now derived from the plugin's own location with path.relative, so it is right wherever the module sits. 2. That fix appeared to do nothing, because officer-net was a dependency and bun COPIES those. Prebuild ran a week-old copy while the source was edited — a clean edit and a stale build, the same shadow-copy trap that cost a night in the monorepo. Making it a workspace turns node_modules/officer-net into a symlink. 3. @babel/plugin-transform-react-jsx was never declared by anything. It was hoisted into the monorepo by some other package, so babel-preset-expo with jsxImportSource found it by luck. 4. Same for react-native-reanimated and react-native-worklets: nativewind depends on react-native-css-interop, which lists reanimated as a PEER, whose babel plugin requires react-native-worklets/plugin. Three levels of nobody-declared-it. 5. And the self-inflicted one. Adding for (2) silently flipped bun to its isolated linker — only direct dependencies at top level, everything else symlinked into node_modules/.bun, which Metro cannot resolve. It surfaced as 'Unable to resolve module react-native-css-interop/jsx-runtime', naming a package that was installed and in the lockfile and simply not where the resolver looks. bunfig.toml now pins both properties that are needed at once: workspaces for the symlink, hoisted for the layout. Result: OffScale.app, 82 MB, with OffTailTunnel.appex at 20 MB carrying 129 Tailcore symbols — the Go core is in the extension, which is the half that matters. Not yet run on a device. It builds; that is a different claim. Co-Authored-By: Claude Opus 5 --- bun.lock | 16 ++++++++-- bunfig.toml | 14 +++++++++ .../plugin/withNetworkExtensionTarget.js | 30 ++++++++++++++----- package.json | 10 +++++-- 4 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 bunfig.toml diff --git a/bun.lock b/bun.lock index b81e47f..58849df 100644 --- a/bun.lock +++ b/bun.lock @@ -14,20 +14,30 @@ "expo-status-bar": "57.0.1", "expo-system-ui": "57.0.1", "nativewind": "4.2.6", - "officer-net": "file:./modules/officer-net", + "officer-net": "workspace:*", "react": "19.2.3", "react-native": "0.86.0", + "react-native-reanimated": "4.5.3", "react-native-safe-area-context": "5.7.0", "react-native-svg": "15.15.4", + "react-native-worklets": "0.11.3", }, "devDependencies": { "@babel/core": "7.29.7", + "@babel/plugin-transform-react-jsx": "7.29.7", "@types/react": "19.2.17", "babel-preset-expo": "57.0.4", "tailwindcss": "3.4.19", "typescript": "6.0.3", }, }, + "modules/officer-net": { + "name": "officer-net", + "version": "0.1.0", + "dependencies": { + "@expo/config-plugins": "^57.0.0", + }, + }, }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -776,7 +786,7 @@ "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], - "officer-net": ["officer-net@file:modules/officer-net", { "dependencies": { "@expo/config-plugins": "^57.0.0" } }], + "officer-net": ["officer-net@workspace:modules/officer-net"], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -858,7 +868,7 @@ "react-native-svg": ["react-native-svg@15.15.4", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A=="], - "react-native-worklets": ["react-native-worklets@0.11.4", "", { "dependencies": { "@babel/generator": "^7.27.1", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-yNiDDQAVvt1wacBhnrEMTBlQzZ8Y8Rg7Dbdxs/r595EXw4REsZGE5sY1Ug8SmlQUT+0L+fX7oY0SuCs0mjiA4w=="], + "react-native-worklets": ["react-native-worklets@0.11.3", "", { "dependencies": { "@babel/generator": "^7.27.1", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.28.6", "@babel/plugin-transform-classes": "^7.28.6", "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.28.5", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.4" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.83 - 0.86" } }, "sha512-paLRlVFTrrGhkGTi37LHwtaJbrTftA54/zAx1h/ONSBw7XejZn6XUUl1KpM6EOMwsbw7TEGuxJXTAp/wkvuMqQ=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..1a349a0 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,14 @@ +# React Native needs a HOISTED node_modules, and declaring `workspaces` silently changes that. +# +# `modules/officer-net` is a workspace so that node_modules/officer-net is a SYMLINK rather than a +# copy — with a `file:` dependency bun copies the package, and an edit to the config plugin then has no +# effect on the next prebuild while looking like it should. That cost three builds to find. +# +# But adding `workspaces` also flips bun to its isolated linker: only direct dependencies sit at the +# top level and everything else is symlinked into node_modules/.bun. Metro cannot resolve that — the +# failure is `Unable to resolve module react-native-css-interop/jsx-runtime`, naming a package that is +# installed, present in the lockfile, and simply not where the resolver looks. +# +# So: workspaces for the link, hoisted for the layout. +[install] +linker = "hoisted" diff --git a/modules/officer-net/plugin/withNetworkExtensionTarget.js b/modules/officer-net/plugin/withNetworkExtensionTarget.js index 2ae62f7..f84b010 100644 --- a/modules/officer-net/plugin/withNetworkExtensionTarget.js +++ b/modules/officer-net/plugin/withNetworkExtensionTarget.js @@ -110,12 +110,26 @@ function withNeTarget(config, props) { proj.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid); proj.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid); - // Link the vendored gomobile Tailscale core (TailCore.xcframework) into the NE. The `xcode` lib has - // no .xcframework extension mapping, so we MUST force lastKnownFileType='wrapper.xcframework' (else it - // types it 'unknown' and Xcode never resolves the per-slice module → "no such module 'TailCore'"). - // Path is relative to SOURCE_ROOT (the ios/ project dir) and includes a dirname so the search path - // is registered. Located in the package's libs/ (../../../ from apps/*/ios up to packages/). - proj.addFramework('../../../packages/officer-net/ios/libs/TailCore.xcframework', { + /* + Link the vendored gomobile Tailscale core (TailCore.xcframework) into the NE. + + The `xcode` lib has no .xcframework extension mapping, so lastKnownFileType MUST be forced to + 'wrapper.xcframework' — left as 'unknown', Xcode never resolves the per-slice module and the build + fails with "no such module 'TailCore'". + + The path is DERIVED, not written down. It used to read `../../../packages/officer-net/ios/libs`, + which encodes one repository's shape — correct from `apps//ios` in the monorepo, and wrong + everywhere else. Moving this module into a repo of its own produced exactly that: the Go core built + cleanly to the right place, then xcodebuild looked for it three levels up from a directory that no + longer had three levels, and reported a path belonging to neither repo. + + Computing it from this file's own location cannot drift: wherever the module sits relative to the + generated ios/ directory, `path.relative` says so. + */ + const libsDir = path.join(__dirname, '..', 'ios', 'libs'); + const iosDir = cfg.modRequest.platformProjectRoot; + const relLibs = path.relative(iosDir, libsDir); + proj.addFramework(path.join(relLibs, 'TailCore.xcframework'), { target: target.uuid, customFramework: true, lastKnownFileType: 'wrapper.xcframework', @@ -126,8 +140,8 @@ function withNeTarget(config, props) { }); const devTeam = mainAppDevTeam(proj); - // Framework search path → the package's libs dir (from ios/: up to the monorepo, into packages). - const fwSearch = '"$(PROJECT_DIR)/../../../packages/officer-net/ios/libs"'; + // Same derivation for the search path — the two must agree, and hardcoding either is how they stop. + const fwSearch = `"$(PROJECT_DIR)/${relLibs}"`; const configs = proj.pbxXCBuildConfigurationSection(); for (const key in configs) { diff --git a/package.json b/package.json index 75ddeda..3fe2bda 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "MIT", "private": true, + "workspaces": [ + "modules/*" + ], "main": "src/index.tsx", "scripts": { "start": "expo start", @@ -21,14 +24,17 @@ "expo-status-bar": "57.0.1", "expo-system-ui": "57.0.1", "nativewind": "4.2.6", - "officer-net": "file:./modules/officer-net", + "officer-net": "workspace:*", "react": "19.2.3", "react-native": "0.86.0", + "react-native-reanimated": "4.5.3", "react-native-safe-area-context": "5.7.0", - "react-native-svg": "15.15.4" + "react-native-svg": "15.15.4", + "react-native-worklets": "0.11.3" }, "devDependencies": { "@babel/core": "7.29.7", + "@babel/plugin-transform-react-jsx": "7.29.7", "@types/react": "19.2.17", "babel-preset-expo": "57.0.4", "tailwindcss": "3.4.19",