@react-native-native/nativ-fabric 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NativFabric.podspec +41 -0
- package/android/build.gradle +128 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +59 -0
- package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
- package/android/src/main/cpp/NativRuntime.cpp +508 -0
- package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
- package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
- package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
- package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
- package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
- package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
- package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
- package/app.plugin.js +159 -0
- package/expo-module.config.json +6 -0
- package/ios/NativContainerComponentView.mm +137 -0
- package/ios/NativRuntime.h +36 -0
- package/ios/NativRuntime.mm +549 -0
- package/metro/Nativ.h +126 -0
- package/metro/compilers/android-compiler.js +339 -0
- package/metro/compilers/dylib-compiler.js +474 -0
- package/metro/compilers/kotlin-compiler.js +632 -0
- package/metro/compilers/rust-compiler.js +722 -0
- package/metro/compilers/static-compiler.js +1118 -0
- package/metro/compilers/swift-compiler.js +363 -0
- package/metro/extractors/cpp-ast-extractor.js +126 -0
- package/metro/extractors/kotlin-extractor.js +125 -0
- package/metro/extractors/rust-extractor.js +118 -0
- package/metro/index.js +236 -0
- package/metro/transformer.js +649 -0
- package/metro/utils/bridge-generator.js +50 -0
- package/metro/utils/compile-commands.js +104 -0
- package/metro/utils/cpp-daemon.js +344 -0
- package/metro/utils/dts-generator.js +32 -0
- package/metro/utils/include-resolver.js +73 -0
- package/metro/utils/kotlin-daemon.js +394 -0
- package/metro/utils/type-mapper.js +63 -0
- package/package.json +43 -0
- package/react-native.config.js +13 -0
- package/src/NativContainerNativeComponent.ts +9 -0
- package/src/NativeNativRuntime.ts +8 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Nativ transformer — routes .rs, .cpp, .mm, .cc files
|
|
3
|
+
* to the appropriate handler. All other files go to the default Expo
|
|
4
|
+
* Babel transformer.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { extractCppExports } = require('./extractors/cpp-ast-extractor');
|
|
10
|
+
const { extractRustExports } = require('./extractors/rust-extractor');
|
|
11
|
+
const { generateDTS } = require('./utils/dts-generator');
|
|
12
|
+
const { getIncludePaths } = require('./utils/include-resolver');
|
|
13
|
+
const { generateCompileCommands } = require('./utils/compile-commands');
|
|
14
|
+
const { compileDylib, compileCppComponentDylib } = require('./compilers/dylib-compiler');
|
|
15
|
+
const { compileRustDylib } = require('./compilers/rust-compiler');
|
|
16
|
+
const { compileAndroidCppDylib, compileAndroidCppComponentDylib, compileAndroidRustDylib } = require('./compilers/android-compiler');
|
|
17
|
+
const { compileKotlinDex, extractKotlinExports } = require('./compilers/kotlin-compiler');
|
|
18
|
+
|
|
19
|
+
// Resolve the default Expo transformer. Since this file lives in the package
|
|
20
|
+
// (not the app), we resolve from process.cwd() which is the app root.
|
|
21
|
+
let upstreamTransformer;
|
|
22
|
+
try {
|
|
23
|
+
const mc = require(require.resolve('expo/metro-config', { paths: [process.cwd()] }));
|
|
24
|
+
const cfg = mc.getDefaultConfig(process.cwd());
|
|
25
|
+
upstreamTransformer = require(cfg.transformer.babelTransformerPath);
|
|
26
|
+
} catch {
|
|
27
|
+
upstreamTransformer = require(require.resolve(
|
|
28
|
+
'@expo/metro-config/babel-transformer',
|
|
29
|
+
{ paths: [process.cwd()] }
|
|
30
|
+
));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Cached per Metro session
|
|
34
|
+
let _includePaths = null;
|
|
35
|
+
let _buildCounter = 0;
|
|
36
|
+
|
|
37
|
+
// ─── Rust component shim ──────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function componentIdForFile(filename) {
|
|
40
|
+
const name = path.basename(filename, '.rs').toLowerCase();
|
|
41
|
+
return `nativ.${name}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rustComponentShim(componentId, srcHash, libExt) {
|
|
45
|
+
const displayName = componentId.split('.').pop();
|
|
46
|
+
const moduleId = componentId.split('.').pop().toLowerCase();
|
|
47
|
+
const _ext = libExt || 'dylib';
|
|
48
|
+
// Fast Refresh swaps function bodies but keeps module-level vars.
|
|
49
|
+
// So we put the hash as a literal inside the function body — when FR
|
|
50
|
+
// patches the function, the new hash is baked into the new body.
|
|
51
|
+
return `
|
|
52
|
+
import React from 'react';
|
|
53
|
+
import NativContainer from '@react-native-native/nativ-fabric/src/NativContainerNativeComponent';
|
|
54
|
+
|
|
55
|
+
if (!global.__nativ_loaded) global.__nativ_loaded = {};
|
|
56
|
+
|
|
57
|
+
const ${displayName} = React.forwardRef((props, ref) => {
|
|
58
|
+
const { style, children, ...nativeProps } = props;
|
|
59
|
+
|
|
60
|
+
// Hot-reload: load dylib when source changes
|
|
61
|
+
const hash = '${srcHash}';
|
|
62
|
+
if (global.__nativ_loaded['${moduleId}'] !== hash) {
|
|
63
|
+
global.__nativ_loaded['${moduleId}'] = hash;
|
|
64
|
+
try {
|
|
65
|
+
const { NativeModules } = require('react-native');
|
|
66
|
+
const _scriptUrl = NativeModules?.SourceCode?.getConstants?.()?.scriptURL || '';
|
|
67
|
+
const _host = _scriptUrl.match(/^https?:\\/\\/[^/]+/)?.[0] || '';
|
|
68
|
+
if (_host && global.__nativ?.loadDylib) {
|
|
69
|
+
const _t = global.__nativ.target || '';
|
|
70
|
+
global.__nativ.loadDylib(_host + '/__nativ_dylib/' + _t + '/nativ_${moduleId}_' + hash + '.${_ext}');
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) console.log('[nativ] dylib load:', e?.message);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Store props in JSI — called on every render (React handles when to re-render)
|
|
78
|
+
if (global.__nativ?.setComponentProps) {
|
|
79
|
+
global.__nativ.setComponentProps('${componentId}', nativeProps);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// DEBUG: use Date.now() to guarantee unique propsJson every render
|
|
83
|
+
// If numbers STILL stop, the issue is not in propsJson comparison
|
|
84
|
+
const _pj = String(Date.now());
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<NativContainer
|
|
88
|
+
style={style}
|
|
89
|
+
ref={ref}
|
|
90
|
+
key={'${srcHash}'}
|
|
91
|
+
componentId="${componentId}"
|
|
92
|
+
propsJson={_pj}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
${displayName}.displayName = '${displayName}';
|
|
97
|
+
|
|
98
|
+
export default ${displayName};
|
|
99
|
+
export { ${displayName} };
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Production shims ─────────────────────────────────────────────────
|
|
104
|
+
// In release builds (dev: false), all native code is statically linked.
|
|
105
|
+
// No loadDylib, no __nativ_loaded tracking, no Metro host URL.
|
|
106
|
+
// Functions are already registered at app start via constructors.
|
|
107
|
+
|
|
108
|
+
function cppFunctionShimProd(exports, moduleId) {
|
|
109
|
+
const lines = [`import '@react-native-native/nativ-fabric';`, ''];
|
|
110
|
+
for (const fn of exports) {
|
|
111
|
+
const argNames = fn.args.map(a => a.name).join(', ');
|
|
112
|
+
if (fn.async) {
|
|
113
|
+
lines.push(
|
|
114
|
+
`export function ${fn.name}(${argNames}) {`,
|
|
115
|
+
` const argsJson = JSON.stringify([${argNames}]);`,
|
|
116
|
+
` return global.__nativ.callAsync('${moduleId}', '${fn.name}', argsJson);`,
|
|
117
|
+
`}`,
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
lines.push(
|
|
121
|
+
`export function ${fn.name}(${argNames}) {`,
|
|
122
|
+
` const argsJson = JSON.stringify([${argNames}]);`,
|
|
123
|
+
` const result = global.__nativ.callSync('${moduleId}', '${fn.name}', argsJson);`,
|
|
124
|
+
` return JSON.parse(result);`,
|
|
125
|
+
`}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rustComponentShimProd(componentId) {
|
|
134
|
+
const displayName = componentId.split('.').pop();
|
|
135
|
+
return `
|
|
136
|
+
import React from 'react';
|
|
137
|
+
import NativContainer from '@react-native-native/nativ-fabric/src/NativContainerNativeComponent';
|
|
138
|
+
|
|
139
|
+
const ${displayName} = React.forwardRef((props, ref) => {
|
|
140
|
+
const { style, children, ...nativeProps } = props;
|
|
141
|
+
|
|
142
|
+
if (global.__nativ?.setComponentProps) {
|
|
143
|
+
global.__nativ.setComponentProps('${componentId}', nativeProps);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<NativContainer
|
|
148
|
+
style={style}
|
|
149
|
+
ref={ref}
|
|
150
|
+
componentId="${componentId}"
|
|
151
|
+
propsJson={JSON.stringify(nativeProps)}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
${displayName}.displayName = '${displayName}';
|
|
156
|
+
|
|
157
|
+
export default ${displayName};
|
|
158
|
+
export { ${displayName} };
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── C++/ObjC++ function shim ─────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function moduleIdForFile(filename, projectRoot) {
|
|
165
|
+
const rel = path.relative(projectRoot, filename);
|
|
166
|
+
return rel
|
|
167
|
+
.replace(/\.(cpp|cc|mm|c)$/, '')
|
|
168
|
+
.replace(/[\/\\]/g, '_')
|
|
169
|
+
.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function cppFunctionShim(exports, moduleId, srcHash, dylibId, libExt) {
|
|
173
|
+
const _dylibId = dylibId || moduleId;
|
|
174
|
+
const _ext = libExt || 'dylib';
|
|
175
|
+
// Ensure @react-native-native/nativ-fabric is imported to trigger TurboModule load → installJSIBindings → global.__nativ
|
|
176
|
+
// Fast Refresh only swaps function bodies — so the hash check and dylib load
|
|
177
|
+
// must be INSIDE each exported function as string literals.
|
|
178
|
+
// global.__nativ_loaded tracks which version is loaded per module.
|
|
179
|
+
const loadSnippet = [
|
|
180
|
+
` if (!global.__nativ_loaded) global.__nativ_loaded = {};`,
|
|
181
|
+
` if (global.__nativ_loaded['${_dylibId}'] !== '${srcHash}') {`,
|
|
182
|
+
` global.__nativ_loaded['${_dylibId}'] = '${srcHash}';`,
|
|
183
|
+
` try {`,
|
|
184
|
+
` var _s = require('react-native').NativeModules?.SourceCode?.getConstants?.()?.scriptURL || '';`,
|
|
185
|
+
` var _h = (_s.match(/^https?:\\/\\/[^/]+/) || [''])[0];`,
|
|
186
|
+
` var _t = global.__nativ?.target || '';`,
|
|
187
|
+
` if (_h && global.__nativ?.loadDylib) {`,
|
|
188
|
+
` global.__nativ.loadDylib(_h + '/__nativ_dylib/' + _t + '/${_dylibId}_${srcHash}.${_ext}');`,
|
|
189
|
+
` }`,
|
|
190
|
+
` } catch(e) {}`,
|
|
191
|
+
` }`,
|
|
192
|
+
].join('\n');
|
|
193
|
+
|
|
194
|
+
const lines = [`import '@react-native-native/nativ-fabric';`, ''];
|
|
195
|
+
|
|
196
|
+
for (const fn of exports) {
|
|
197
|
+
const argNames = fn.args.map(a => a.name).join(', ');
|
|
198
|
+
|
|
199
|
+
if (fn.async) {
|
|
200
|
+
lines.push(
|
|
201
|
+
`export function ${fn.name}(${argNames}) {`,
|
|
202
|
+
loadSnippet,
|
|
203
|
+
` const argsJson = JSON.stringify([${argNames}]);`,
|
|
204
|
+
` return global.__nativ.callAsync('${moduleId}', '${fn.name}', argsJson);`,
|
|
205
|
+
`}`,
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
lines.push(
|
|
209
|
+
`export function ${fn.name}(${argNames}) {`,
|
|
210
|
+
loadSnippet,
|
|
211
|
+
` const argsJson = JSON.stringify([${argNames}]);`,
|
|
212
|
+
` const result = global.__nativ.callSync('${moduleId}', '${fn.name}', argsJson);`,
|
|
213
|
+
` return JSON.parse(result);`,
|
|
214
|
+
`}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
lines.push('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Transform entry point ────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
// Content-addressed caching: same source → same hash → same JS shim.
|
|
226
|
+
// Metro's SHA1 cache naturally deduplicates. Undo (A→B→A) serves the
|
|
227
|
+
// cached shim for A, which references the correct binary (also hashed).
|
|
228
|
+
const _sessionId = Date.now().toString(36);
|
|
229
|
+
module.exports.getCacheKey = function () {
|
|
230
|
+
return `nativ-transformer-${_sessionId}`;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
module.exports.transform = async function nativTransform({
|
|
234
|
+
filename,
|
|
235
|
+
src,
|
|
236
|
+
options,
|
|
237
|
+
...rest
|
|
238
|
+
}) {
|
|
239
|
+
const projectRoot = options.projectRoot;
|
|
240
|
+
const platform = options.platform || 'ios';
|
|
241
|
+
const isAndroid = platform === 'android';
|
|
242
|
+
const isDev = options.dev !== false;
|
|
243
|
+
|
|
244
|
+
// Read last-known target (written at startup + updated by middleware on device switch)
|
|
245
|
+
let buildTarget;
|
|
246
|
+
try {
|
|
247
|
+
const targetFile = path.join(projectRoot, `.nativ/${isAndroid ? 'android' : 'ios'}-target`);
|
|
248
|
+
buildTarget = fs.readFileSync(targetFile, 'utf8').trim();
|
|
249
|
+
} catch {}
|
|
250
|
+
if (!buildTarget) buildTarget = isAndroid ? 'arm64-v8a' : 'device';
|
|
251
|
+
|
|
252
|
+
const isNative = filename.startsWith(projectRoot) &&
|
|
253
|
+
!filename.includes('node_modules') &&
|
|
254
|
+
!filename.includes('/packages/') &&
|
|
255
|
+
(filename.endsWith('.rs') || filename.endsWith('.cpp') ||
|
|
256
|
+
filename.endsWith('.cc') || filename.endsWith('.mm') ||
|
|
257
|
+
filename.endsWith('.swift') || filename.endsWith('.kt'));
|
|
258
|
+
|
|
259
|
+
// Skip platform-incompatible files (but still generate .d.ts for IDE)
|
|
260
|
+
if (isAndroid && (filename.endsWith('.swift') || filename.endsWith('.mm'))) {
|
|
261
|
+
if (isDev && filename.endsWith('.swift')) {
|
|
262
|
+
try {
|
|
263
|
+
const { extractSwiftExports } = require('./compilers/swift-compiler');
|
|
264
|
+
const swExports = extractSwiftExports(filename);
|
|
265
|
+
if (swExports.length > 0) {
|
|
266
|
+
const lines = ['// Auto-generated by React Native Native', ''];
|
|
267
|
+
for (const fn of swExports) {
|
|
268
|
+
lines.push(`export declare function ${fn.name}(): ${fn.async ? 'Promise<string>' : 'string'};`);
|
|
269
|
+
}
|
|
270
|
+
lines.push('');
|
|
271
|
+
fs.writeFileSync(dtsPath(filename), lines.join('\n'));
|
|
272
|
+
}
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
return upstreamTransformer.transform({
|
|
276
|
+
filename: filename.replace(/\.(swift|mm)$/, '.js'),
|
|
277
|
+
src: 'export default undefined;\n',
|
|
278
|
+
options, ...rest,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (!isAndroid && filename.endsWith('.kt')) {
|
|
282
|
+
// Still generate .d.ts for IDE support even on iOS
|
|
283
|
+
if (isDev) {
|
|
284
|
+
try {
|
|
285
|
+
const { functions: ktFns, isComponent: ktIsComp, componentProps: ktProps } = extractKotlinExports(filename);
|
|
286
|
+
const ktBaseName = path.basename(filename, '.kt');
|
|
287
|
+
if (ktIsComp) {
|
|
288
|
+
const propsLines = ktProps.filter(p => !p.isCallback).map(p => ` ${p.jsName}?: ${p.tsType};`);
|
|
289
|
+
const cbLines = ktProps.filter(p => p.isCallback).map(p => ` ${p.jsName}?: () => void;`);
|
|
290
|
+
fs.writeFileSync(dtsPath(filename), [
|
|
291
|
+
`import type { ViewProps } from 'react-native';`, '',
|
|
292
|
+
`interface ${ktBaseName}Props extends ViewProps {`, ...propsLines, ...cbLines, `}`, '',
|
|
293
|
+
`declare const ${ktBaseName}: React.ComponentType<${ktBaseName}Props>;`,
|
|
294
|
+
`export default ${ktBaseName};`, `export { ${ktBaseName} };`, '',
|
|
295
|
+
].join('\n'));
|
|
296
|
+
} else if (ktFns.length > 0) {
|
|
297
|
+
const lines = ['// Auto-generated by React Native Native', ''];
|
|
298
|
+
for (const fn of ktFns) {
|
|
299
|
+
const args = fn.args.map(a => `${a.name}: ${a.tsType}`).join(', ');
|
|
300
|
+
lines.push(`export declare function ${fn.name}(${args}): ${fn.tsType};`);
|
|
301
|
+
}
|
|
302
|
+
lines.push('');
|
|
303
|
+
fs.writeFileSync(dtsPath(filename), lines.join('\n'));
|
|
304
|
+
}
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
|
307
|
+
return upstreamTransformer.transform({
|
|
308
|
+
filename: filename.replace(/\.kt$/, '.js'),
|
|
309
|
+
src: 'export default undefined;\n',
|
|
310
|
+
options, ...rest,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (isNative) {
|
|
315
|
+
console.log(`[nativ] transform (${platform}): ${path.basename(filename)} (build #${++_buildCounter})`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── .d.ts output path ────────────────────────────────────────────────
|
|
319
|
+
// Write typings to .nativ/typings/, stripping native extension.
|
|
320
|
+
// tsconfig rootDirs: [".", ".nativ/typings"] makes TS find them.
|
|
321
|
+
// e.g. math_utils.cpp → .nativ/typings/math_utils.d.ts
|
|
322
|
+
function dtsPath(sourceFile) {
|
|
323
|
+
const rel = path.relative(projectRoot, sourceFile);
|
|
324
|
+
const dtsRel = rel.replace(/\.(rs|cpp|cc|mm|c|swift|kt)$/, '.d.ts');
|
|
325
|
+
const out = path.join(projectRoot, '.nativ/typings', dtsRel);
|
|
326
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Content-addressed compile + manifest ─────────────────────────────
|
|
331
|
+
// Compiles for the last-known target (single flash hot-reload).
|
|
332
|
+
// Also writes manifest so the middleware can compile on-demand for other targets.
|
|
333
|
+
const manifestPath = path.join(projectRoot, '.nativ/modules.json');
|
|
334
|
+
function writeManifest(dylibName, entry) {
|
|
335
|
+
let manifest = {};
|
|
336
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch {}
|
|
337
|
+
manifest[dylibName] = entry;
|
|
338
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
339
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function cachedCompile(srcContent, origName, ext, compileFn) {
|
|
343
|
+
const hash = require('crypto').createHash('md5').update(srcContent).digest('hex').slice(0, 8);
|
|
344
|
+
const hashedName = `${origName}_${hash}.${ext}`;
|
|
345
|
+
const dylibDir = path.join(projectRoot, '.nativ/dylibs', buildTarget);
|
|
346
|
+
fs.mkdirSync(dylibDir, { recursive: true });
|
|
347
|
+
const hashedPath = path.join(dylibDir, hashedName);
|
|
348
|
+
if (fs.existsSync(hashedPath)) {
|
|
349
|
+
console.log(`[nativ] ${origName} cache hit (${hash}, ${buildTarget})`);
|
|
350
|
+
return hash;
|
|
351
|
+
}
|
|
352
|
+
compileFn(); // compile for last-known target
|
|
353
|
+
const origPath = path.join(dylibDir, `${origName}.${ext}`);
|
|
354
|
+
if (fs.existsSync(origPath)) {
|
|
355
|
+
try { fs.copyFileSync(origPath, hashedPath); } catch {}
|
|
356
|
+
}
|
|
357
|
+
return hash;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Rust files → component or function shim
|
|
361
|
+
if (filename.endsWith('.rs')) {
|
|
362
|
+
const { functions, isComponent, componentProps } = extractRustExports(filename);
|
|
363
|
+
const baseName = path.basename(filename, '.rs').toLowerCase();
|
|
364
|
+
|
|
365
|
+
let srcHash = 'prod';
|
|
366
|
+
if (isDev) {
|
|
367
|
+
const libExt = isAndroid ? 'so' : 'dylib';
|
|
368
|
+
srcHash = cachedCompile(src, `nativ_${baseName}`, libExt, () => {
|
|
369
|
+
if (isAndroid) compileAndroidRustDylib(filename, projectRoot, { target: buildTarget });
|
|
370
|
+
else compileRustDylib(filename, projectRoot, { target: buildTarget });
|
|
371
|
+
});
|
|
372
|
+
writeManifest(`nativ_${baseName}`, { source: filename, type: isComponent ? 'rust-component' : 'rust' });
|
|
373
|
+
// Generate .d.ts for TypeScript support
|
|
374
|
+
try {
|
|
375
|
+
if (isComponent) {
|
|
376
|
+
const displayName = path.basename(filename, '.rs');
|
|
377
|
+
const propsLines = componentProps.map(p => ` ${p.jsName}?: ${p.tsType};`);
|
|
378
|
+
const dts = [
|
|
379
|
+
`import type { ViewProps } from 'react-native';`,
|
|
380
|
+
``,
|
|
381
|
+
`interface ${displayName}Props extends ViewProps {`,
|
|
382
|
+
...propsLines,
|
|
383
|
+
`}`,
|
|
384
|
+
``,
|
|
385
|
+
`declare const ${displayName}: React.ComponentType<${displayName}Props>;`,
|
|
386
|
+
`export default ${displayName};`,
|
|
387
|
+
`export { ${displayName} };`,
|
|
388
|
+
``,
|
|
389
|
+
].join('\n');
|
|
390
|
+
fs.writeFileSync(dtsPath(filename), dts);
|
|
391
|
+
} else if (functions.length > 0) {
|
|
392
|
+
fs.writeFileSync(dtsPath(filename), generateDTS(functions));
|
|
393
|
+
}
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const _libExt = isAndroid ? 'so' : 'dylib';
|
|
398
|
+
let shimCode;
|
|
399
|
+
if (isComponent) {
|
|
400
|
+
const componentId = componentIdForFile(filename);
|
|
401
|
+
shimCode = isDev
|
|
402
|
+
? rustComponentShim(componentId, srcHash, _libExt)
|
|
403
|
+
: rustComponentShimProd(componentId);
|
|
404
|
+
} else if (functions.length > 0) {
|
|
405
|
+
const moduleId = `nativ.${baseName}`;
|
|
406
|
+
const fns = functions.map(f => ({ ...f, args: f.args.map(a => ({ ...a, type: a.tsType || a.type })) }));
|
|
407
|
+
shimCode = isDev
|
|
408
|
+
? cppFunctionShim(fns, moduleId, srcHash, `nativ_${baseName}`, _libExt)
|
|
409
|
+
: cppFunctionShimProd(fns, moduleId);
|
|
410
|
+
} else {
|
|
411
|
+
shimCode = `// ${path.basename(filename)}: no exports found\nexport {};\n`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return upstreamTransformer.transform({
|
|
415
|
+
filename: filename.replace(/\.rs$/, '.js'),
|
|
416
|
+
src: shimCode,
|
|
417
|
+
options,
|
|
418
|
+
...rest,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Swift files → function or component shim
|
|
423
|
+
if (filename.endsWith('.swift')) {
|
|
424
|
+
const { compileSwiftDylib, extractSwiftExports } = require('./compilers/swift-compiler');
|
|
425
|
+
|
|
426
|
+
const swiftSrc = fs.readFileSync(filename, 'utf8');
|
|
427
|
+
const isSwiftComponent = swiftSrc.includes('@nativ_component') || swiftSrc.includes('nativ::component');
|
|
428
|
+
const moduleId = path.basename(filename, '.swift').toLowerCase();
|
|
429
|
+
|
|
430
|
+
let srcHash = 'prod';
|
|
431
|
+
if (isDev) {
|
|
432
|
+
const origName = isSwiftComponent ? `nativ_${moduleId}` : moduleId;
|
|
433
|
+
srcHash = cachedCompile(src, origName, 'dylib', () => {
|
|
434
|
+
compileSwiftDylib(filename, projectRoot, { target: buildTarget });
|
|
435
|
+
});
|
|
436
|
+
writeManifest(origName, { source: filename, type: isSwiftComponent ? 'swift-component' : 'swift' });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (isSwiftComponent) {
|
|
440
|
+
const componentId = `nativ.${moduleId}`;
|
|
441
|
+
const shimCode = isDev
|
|
442
|
+
? rustComponentShim(componentId, srcHash)
|
|
443
|
+
: rustComponentShimProd(componentId);
|
|
444
|
+
return upstreamTransformer.transform({
|
|
445
|
+
filename: filename.replace(/\.swift$/, '.js'),
|
|
446
|
+
src: shimCode,
|
|
447
|
+
options,
|
|
448
|
+
...rest,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const exports = extractSwiftExports(filename);
|
|
453
|
+
|
|
454
|
+
if (isDev) {
|
|
455
|
+
try {
|
|
456
|
+
const lines = ['// Auto-generated by React Native Native', ''];
|
|
457
|
+
for (const fn of exports) {
|
|
458
|
+
lines.push(`export declare function ${fn.name}(): ${fn.async ? 'Promise<string>' : 'string'};`);
|
|
459
|
+
}
|
|
460
|
+
lines.push('');
|
|
461
|
+
fs.writeFileSync(dtsPath(filename), lines.join('\n'));
|
|
462
|
+
} catch {}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const fns = exports.map(fn => ({ name: fn.name, async: fn.async, args: [] }));
|
|
466
|
+
const shimCode = isDev
|
|
467
|
+
? cppFunctionShim(fns, moduleId, srcHash, moduleId)
|
|
468
|
+
: cppFunctionShimProd(fns, moduleId);
|
|
469
|
+
|
|
470
|
+
return upstreamTransformer.transform({
|
|
471
|
+
filename: filename.replace(/\.swift$/, '.js'),
|
|
472
|
+
src: shimCode,
|
|
473
|
+
options,
|
|
474
|
+
...rest,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── C++/ObjC++ files → function export shim or component
|
|
479
|
+
const isCpp = filename.endsWith('.cpp') || filename.endsWith('.cc');
|
|
480
|
+
const isObjCpp = filename.endsWith('.mm');
|
|
481
|
+
|
|
482
|
+
if (isCpp || isObjCpp) {
|
|
483
|
+
// Resolve include paths once + generate compile_commands.json for clangd
|
|
484
|
+
if (isDev && !_includePaths) {
|
|
485
|
+
_includePaths = getIncludePaths(projectRoot);
|
|
486
|
+
generateCompileCommands(projectRoot, _includePaths);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Check if this is a component (NATIV_COMPONENT / nativ::component)
|
|
490
|
+
const { isCppComponent, extractCppComponentProps } = require('./extractors/cpp-ast-extractor');
|
|
491
|
+
if (isCppComponent(filename)) {
|
|
492
|
+
const baseName = path.basename(filename).replace(/\.(cpp|cc|mm)$/, '').toLowerCase();
|
|
493
|
+
const componentId = `nativ.${baseName}`;
|
|
494
|
+
|
|
495
|
+
let srcHash = 'prod';
|
|
496
|
+
const cppProps = extractCppComponentProps(filename);
|
|
497
|
+
if (isDev) {
|
|
498
|
+
const _cppCompLibExt = isAndroid ? 'so' : 'dylib';
|
|
499
|
+
srcHash = cachedCompile(src, `nativ_${baseName}`, _cppCompLibExt, () => {
|
|
500
|
+
if (isAndroid) compileAndroidCppComponentDylib(filename, _includePaths, projectRoot, baseName, cppProps, { target: buildTarget });
|
|
501
|
+
else compileCppComponentDylib(filename, _includePaths, projectRoot, baseName, cppProps, { target: buildTarget });
|
|
502
|
+
});
|
|
503
|
+
writeManifest(`nativ_${baseName}`, { source: filename, type: 'cpp-component', baseName });
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const displayName = path.basename(filename).replace(/\.(cpp|cc|mm)$/, '');
|
|
507
|
+
const propsLines = cppProps.map(p => ` ${p.jsName}?: ${p.tsType};`);
|
|
508
|
+
const dts = [
|
|
509
|
+
`import type { ViewProps } from 'react-native';`,
|
|
510
|
+
``,
|
|
511
|
+
`interface ${displayName}Props extends ViewProps {`,
|
|
512
|
+
...propsLines,
|
|
513
|
+
`}`,
|
|
514
|
+
``,
|
|
515
|
+
`declare const ${displayName}: React.ComponentType<${displayName}Props>;`,
|
|
516
|
+
`export default ${displayName};`,
|
|
517
|
+
`export { ${displayName} };`,
|
|
518
|
+
``,
|
|
519
|
+
].join('\n');
|
|
520
|
+
fs.writeFileSync(dtsPath(filename), dts);
|
|
521
|
+
} catch {}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const _cppLibExt = isAndroid ? 'so' : 'dylib';
|
|
525
|
+
const shimCode = isDev
|
|
526
|
+
? rustComponentShim(componentId, srcHash, _cppLibExt)
|
|
527
|
+
: rustComponentShimProd(componentId);
|
|
528
|
+
|
|
529
|
+
return upstreamTransformer.transform({
|
|
530
|
+
filename: filename.replace(/\.(cpp|cc|mm)$/, '.js'),
|
|
531
|
+
src: shimCode,
|
|
532
|
+
options,
|
|
533
|
+
...rest,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Extract exported functions
|
|
538
|
+
const exports = extractCppExports(filename, _includePaths);
|
|
539
|
+
|
|
540
|
+
if (isDev && exports.length === 0) {
|
|
541
|
+
console.warn(`[nativ] No NATIV_EXPORT functions found in ${path.basename(filename)}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const moduleId = moduleIdForFile(filename, projectRoot);
|
|
545
|
+
|
|
546
|
+
let srcHash = 'prod';
|
|
547
|
+
if (isDev) {
|
|
548
|
+
const _cppFnLibExt = isAndroid ? 'so' : 'dylib';
|
|
549
|
+
srcHash = cachedCompile(src, moduleId, _cppFnLibExt, () => {
|
|
550
|
+
if (exports.length > 0) {
|
|
551
|
+
if (isAndroid) compileAndroidCppDylib(filename, _includePaths, exports, projectRoot, { target: buildTarget });
|
|
552
|
+
else compileDylib(filename, _includePaths, exports, projectRoot, { target: buildTarget });
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
writeManifest(moduleId, { source: filename, type: 'cpp' });
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
fs.writeFileSync(dtsPath(filename), generateDTS(exports));
|
|
559
|
+
} catch {}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const shimCode = isDev
|
|
563
|
+
? cppFunctionShim(exports, moduleId, srcHash, null, isAndroid ? 'so' : 'dylib')
|
|
564
|
+
: cppFunctionShimProd(exports, moduleId);
|
|
565
|
+
|
|
566
|
+
return upstreamTransformer.transform({
|
|
567
|
+
filename: filename.replace(/\.(cpp|cc|mm)$/, '.js'),
|
|
568
|
+
src: shimCode,
|
|
569
|
+
options,
|
|
570
|
+
...rest,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── Kotlin files → function or component shim
|
|
575
|
+
if (filename.endsWith('.kt')) {
|
|
576
|
+
const { functions, isComponent, componentProps } = extractKotlinExports(filename);
|
|
577
|
+
const baseName = path.basename(filename, '.kt');
|
|
578
|
+
const moduleId = baseName.toLowerCase();
|
|
579
|
+
|
|
580
|
+
let srcHash = 'prod';
|
|
581
|
+
if (isDev) {
|
|
582
|
+
srcHash = require('crypto').createHash('md5').update(src).digest('hex').slice(0, 8);
|
|
583
|
+
if (isAndroid) {
|
|
584
|
+
compileKotlinDex(filename, projectRoot);
|
|
585
|
+
}
|
|
586
|
+
writeManifest(moduleId, { source: filename, type: isComponent ? 'kotlin-component' : 'kotlin' });
|
|
587
|
+
|
|
588
|
+
// Generate .d.ts
|
|
589
|
+
try {
|
|
590
|
+
if (isComponent) {
|
|
591
|
+
const propsLines = componentProps
|
|
592
|
+
.filter(p => !p.isCallback)
|
|
593
|
+
.map(p => ` ${p.jsName}?: ${p.tsType};`);
|
|
594
|
+
const cbLines = componentProps
|
|
595
|
+
.filter(p => p.isCallback)
|
|
596
|
+
.map(p => ` ${p.jsName}?: () => void;`);
|
|
597
|
+
const dts = [
|
|
598
|
+
`import type { ViewProps } from 'react-native';`,
|
|
599
|
+
``,
|
|
600
|
+
`interface ${baseName}Props extends ViewProps {`,
|
|
601
|
+
...propsLines,
|
|
602
|
+
...cbLines,
|
|
603
|
+
`}`,
|
|
604
|
+
``,
|
|
605
|
+
`declare const ${baseName}: React.ComponentType<${baseName}Props>;`,
|
|
606
|
+
`export default ${baseName};`,
|
|
607
|
+
`export { ${baseName} };`,
|
|
608
|
+
``,
|
|
609
|
+
].join('\n');
|
|
610
|
+
fs.writeFileSync(dtsPath(filename), dts);
|
|
611
|
+
} else if (functions.length > 0) {
|
|
612
|
+
const lines = ['// Auto-generated by React Native Native', ''];
|
|
613
|
+
for (const fn of functions) {
|
|
614
|
+
const args = fn.args.map(a => `${a.name}: ${a.tsType}`).join(', ');
|
|
615
|
+
const ret = fn.async ? `Promise<${fn.tsType}>` : fn.tsType;
|
|
616
|
+
lines.push(`export declare function ${fn.name}(${args}): ${ret};`);
|
|
617
|
+
}
|
|
618
|
+
lines.push('');
|
|
619
|
+
fs.writeFileSync(dtsPath(filename), lines.join('\n'));
|
|
620
|
+
}
|
|
621
|
+
} catch {}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let shimCode;
|
|
625
|
+
if (isComponent) {
|
|
626
|
+
const componentId = `nativ.${moduleId}`;
|
|
627
|
+
shimCode = isDev
|
|
628
|
+
? rustComponentShim(componentId, srcHash, 'dex')
|
|
629
|
+
: rustComponentShimProd(componentId);
|
|
630
|
+
} else if (functions.length > 0) {
|
|
631
|
+
const fns = functions.map(f => ({ ...f, args: f.args.map(a => ({ ...a, type: a.tsType })) }));
|
|
632
|
+
shimCode = isDev
|
|
633
|
+
? cppFunctionShim(fns, moduleId, srcHash, moduleId, 'dex')
|
|
634
|
+
: cppFunctionShimProd(fns, moduleId);
|
|
635
|
+
} else {
|
|
636
|
+
shimCode = `// ${baseName}.kt: no exports found\nexport {};\n`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return upstreamTransformer.transform({
|
|
640
|
+
filename: filename.replace(/\.kt$/, '.js'),
|
|
641
|
+
src: shimCode,
|
|
642
|
+
options,
|
|
643
|
+
...rest,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ── All other files → upstream Babel
|
|
648
|
+
return upstreamTransformer.transform({ filename, src, options, ...rest });
|
|
649
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates C ABI bridge wrappers for NATIV_EXPORT-annotated functions.
|
|
3
|
+
* Each exported function gets a C-linkage wrapper that the JS runtime calls.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function generateCppBridge(exports, moduleId) {
|
|
7
|
+
const safeModuleId = moduleId.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
8
|
+
const lines = [
|
|
9
|
+
`// Auto-generated bridge for ${moduleId}`,
|
|
10
|
+
'#include "Nativ.h"',
|
|
11
|
+
'#include <string>',
|
|
12
|
+
'',
|
|
13
|
+
'extern "C" {',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const fn of exports) {
|
|
17
|
+
if (fn.async) {
|
|
18
|
+
lines.push(`
|
|
19
|
+
void nativ_${safeModuleId}_${fn.name}(
|
|
20
|
+
const char* argsJson,
|
|
21
|
+
void (*resolve)(const char*),
|
|
22
|
+
void (*reject)(const char*, const char*)
|
|
23
|
+
) {
|
|
24
|
+
try {
|
|
25
|
+
auto result = ${fn.name}(/* TODO: parse args from argsJson */);
|
|
26
|
+
auto json = nativ::toJson(result);
|
|
27
|
+
resolve(json.c_str());
|
|
28
|
+
} catch (const std::exception& e) {
|
|
29
|
+
reject("NATIV_ERROR", e.what());
|
|
30
|
+
} catch (...) {
|
|
31
|
+
reject("NATIV_ERROR", "Unknown error");
|
|
32
|
+
}
|
|
33
|
+
}`);
|
|
34
|
+
} else {
|
|
35
|
+
lines.push(`
|
|
36
|
+
const char* nativ_${safeModuleId}_${fn.name}(const char* argsJson) {
|
|
37
|
+
auto result = ${fn.name}(/* TODO: parse args from argsJson */);
|
|
38
|
+
static thread_local std::string buf;
|
|
39
|
+
buf = nativ::toJson(result);
|
|
40
|
+
return buf.c_str();
|
|
41
|
+
}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push('} // extern "C"');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { generateCppBridge };
|