@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.
Files changed (42) hide show
  1. package/NativFabric.podspec +41 -0
  2. package/android/build.gradle +128 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/cpp/CMakeLists.txt +59 -0
  5. package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
  6. package/android/src/main/cpp/NativRuntime.cpp +508 -0
  7. package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
  8. package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
  9. package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
  10. package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
  11. package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
  12. package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
  13. package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
  14. package/app.plugin.js +159 -0
  15. package/expo-module.config.json +6 -0
  16. package/ios/NativContainerComponentView.mm +137 -0
  17. package/ios/NativRuntime.h +36 -0
  18. package/ios/NativRuntime.mm +549 -0
  19. package/metro/Nativ.h +126 -0
  20. package/metro/compilers/android-compiler.js +339 -0
  21. package/metro/compilers/dylib-compiler.js +474 -0
  22. package/metro/compilers/kotlin-compiler.js +632 -0
  23. package/metro/compilers/rust-compiler.js +722 -0
  24. package/metro/compilers/static-compiler.js +1118 -0
  25. package/metro/compilers/swift-compiler.js +363 -0
  26. package/metro/extractors/cpp-ast-extractor.js +126 -0
  27. package/metro/extractors/kotlin-extractor.js +125 -0
  28. package/metro/extractors/rust-extractor.js +118 -0
  29. package/metro/index.js +236 -0
  30. package/metro/transformer.js +649 -0
  31. package/metro/utils/bridge-generator.js +50 -0
  32. package/metro/utils/compile-commands.js +104 -0
  33. package/metro/utils/cpp-daemon.js +344 -0
  34. package/metro/utils/dts-generator.js +32 -0
  35. package/metro/utils/include-resolver.js +73 -0
  36. package/metro/utils/kotlin-daemon.js +394 -0
  37. package/metro/utils/type-mapper.js +63 -0
  38. package/package.json +43 -0
  39. package/react-native.config.js +13 -0
  40. package/src/NativContainerNativeComponent.ts +9 -0
  41. package/src/NativeNativRuntime.ts +8 -0
  42. package/src/index.ts +4 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Generates compile_commands.json for clangd IDE support.
3
+ *
4
+ * Called once when the Metro transformer first encounters a C++/ObjC++ file.
5
+ * Discovers all .cpp/.cc/.mm/.c files in the project (excluding node_modules,
6
+ * Pods, .nativ) and writes a compile_commands.json at the project root so
7
+ * clangd picks up the correct include paths, language standard, and sysroot.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ function findNativeFiles(projectRoot) {
14
+ const results = [];
15
+ const ignore = new Set(['node_modules', 'ios', 'android', '.nativ', '.git', 'build']);
16
+
17
+ function walk(dir) {
18
+ let entries;
19
+ try {
20
+ entries = fs.readdirSync(dir, { withFileTypes: true });
21
+ } catch {
22
+ return;
23
+ }
24
+
25
+ for (const entry of entries) {
26
+ if (entry.name.startsWith('.') && entry.name !== '.nativ') continue;
27
+ if (ignore.has(entry.name) && dir === projectRoot) continue;
28
+
29
+ const full = path.join(dir, entry.name);
30
+
31
+ if (entry.isDirectory()) {
32
+ // Don't recurse into ignored dirs at any level
33
+ if (entry.name === 'node_modules' || entry.name === 'Pods') continue;
34
+ walk(full);
35
+ } else if (/\.(cpp|cc|mm|c|h|hpp)$/.test(entry.name)) {
36
+ results.push(full);
37
+ }
38
+ }
39
+ }
40
+
41
+ walk(projectRoot);
42
+ return results;
43
+ }
44
+
45
+ function generateCompileCommands(projectRoot, includePaths) {
46
+ const files = findNativeFiles(projectRoot);
47
+ if (files.length === 0) return;
48
+
49
+ const entries = files
50
+ .filter(f => /\.(cpp|cc|mm|c)$/.test(f)) // only source files, not headers
51
+ .map(file => {
52
+ const isObjCpp = file.endsWith('.mm');
53
+ const isC = file.endsWith('.c');
54
+ const lang = isObjCpp ? 'objective-c++' : isC ? 'c' : 'c++';
55
+
56
+ // Include the package's metro/ dir for Nativ.h
57
+ const nativHeaderDir = path.resolve(__dirname, '..');
58
+
59
+ const args = [
60
+ 'clang++',
61
+ '-x', lang,
62
+ '-std=c++17',
63
+ '-arch', 'arm64',
64
+ '-I' + nativHeaderDir,
65
+ ];
66
+
67
+ // ObjC++ files need the iOS SDK sysroot for UIKit/Foundation headers.
68
+ // Pure C++ files work better without it (host stdlib).
69
+ if (isObjCpp) {
70
+ args.push(...includePaths);
71
+ args.push('-fmodules');
72
+ } else {
73
+ // Strip -isysroot for pure C++ (use host headers)
74
+ for (let i = 0; i < includePaths.length; i++) {
75
+ if (includePaths[i] === '-isysroot') { i++; continue; }
76
+ args.push(includePaths[i]);
77
+ }
78
+ }
79
+
80
+ args.push('-c', file);
81
+
82
+ return {
83
+ directory: projectRoot,
84
+ file: file,
85
+ arguments: args,
86
+ };
87
+ });
88
+
89
+ const outPath = path.join(projectRoot, 'compile_commands.json');
90
+ const json = JSON.stringify(entries, null, 2);
91
+
92
+ // Only write if changed (avoid triggering unnecessary clangd restarts)
93
+ try {
94
+ const existing = fs.readFileSync(outPath, 'utf8');
95
+ if (existing === json) return;
96
+ } catch {
97
+ // File doesn't exist yet
98
+ }
99
+
100
+ fs.writeFileSync(outPath, json);
101
+ console.log(`[nativ] Generated compile_commands.json (${entries.length} files)`);
102
+ }
103
+
104
+ module.exports = { generateCompileCommands };
@@ -0,0 +1,344 @@
1
+ /**
2
+ * CppDaemon — watches .cpp/.mm/.cc files, compiles to signed arm64 dylibs,
3
+ * and signals Metro when builds complete so HMR can fire.
4
+ *
5
+ * The compiled dylib is served by Metro's dev server. The app downloads it
6
+ * to its sandbox and dlopen's it — the __attribute__((constructor)) in the
7
+ * bridge re-registers functions, replacing the statically-linked versions.
8
+ */
9
+
10
+ const { execSync, exec } = require('child_process');
11
+ const EventEmitter = require('events');
12
+ const crypto = require('crypto');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+
16
+ class CppDaemon extends EventEmitter {
17
+ constructor(projectRoot, includePaths) {
18
+ super();
19
+ this.projectRoot = projectRoot;
20
+ this.includePaths = includePaths;
21
+ this.building = new Set();
22
+ this.outputDir = path.join(projectRoot, '.nativ/dylibs');
23
+ this.sdkPath = null;
24
+ this.signingIdentity = null;
25
+ this._watcher = null;
26
+ }
27
+
28
+ start() {
29
+ fs.mkdirSync(this.outputDir, { recursive: true });
30
+
31
+ // Resolve SDK and signing identity once
32
+ try {
33
+ this.sdkPath = execSync('xcrun --sdk iphoneos --show-sdk-path', {
34
+ encoding: 'utf8',
35
+ }).trim();
36
+ } catch {
37
+ console.warn('[nativ] Could not resolve iphoneos SDK, falling back to iphonesimulator');
38
+ try {
39
+ this.sdkPath = execSync('xcrun --sdk iphonesimulator --show-sdk-path', {
40
+ encoding: 'utf8',
41
+ }).trim();
42
+ } catch {
43
+ console.error('[nativ] No iOS SDK found');
44
+ return;
45
+ }
46
+ }
47
+
48
+ try {
49
+ const identities = execSync('security find-identity -v -p codesigning', {
50
+ encoding: 'utf8',
51
+ });
52
+ const match = identities.match(/"(Apple Development:[^"]+)"/);
53
+ if (match) {
54
+ this.signingIdentity = match[1];
55
+ console.log(`[nativ] Signing identity: ${this.signingIdentity}`);
56
+ }
57
+ } catch {
58
+ console.warn('[nativ] No signing identity found — dylibs will be unsigned');
59
+ }
60
+
61
+ // No file watcher needed — Metro's watcher sees .cpp changes and the
62
+ // transformer calls buildAndWait() directly. This avoids double-refresh.
63
+ console.log('[nativ] CppDaemon started');
64
+ }
65
+
66
+ _startWatcher() {
67
+ // Use fs.watch recursively — no chokidar dependency needed
68
+ const watchDirs = [this.projectRoot];
69
+ const ignore = new Set(['node_modules', 'ios', 'android', '.nativ', '.git', 'build', 'Pods']);
70
+
71
+ const self = this;
72
+
73
+ function watchDir(dir) {
74
+ try {
75
+ const watcher = fs.watch(dir, { recursive: false }, (eventType, filename) => {
76
+ if (!filename) return;
77
+ if (/\.(cpp|cc|mm)$/.test(filename)) {
78
+ const fullPath = path.join(dir, filename);
79
+ if (fs.existsSync(fullPath)) {
80
+ self._onFileChange(fullPath);
81
+ }
82
+ }
83
+ });
84
+ // Watch subdirectories
85
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
86
+ if (entry.isDirectory() && !ignore.has(entry.name) && !entry.name.startsWith('.')) {
87
+ watchDir(path.join(dir, entry.name));
88
+ }
89
+ }
90
+ } catch {
91
+ // Permission errors, etc.
92
+ }
93
+ }
94
+
95
+ watchDir(this.projectRoot);
96
+ }
97
+
98
+ _onFileChange(filepath) {
99
+ // Debounce — skip if already building this file
100
+ if (this.building.has(filepath)) return;
101
+
102
+ // Only rebuild files that have NATIV_EXPORT
103
+ try {
104
+ const src = fs.readFileSync(filepath, 'utf8');
105
+ if (!src.includes('NATIV_EXPORT')) return;
106
+ } catch {
107
+ return;
108
+ }
109
+
110
+ console.log(`[nativ] File changed: ${path.relative(this.projectRoot, filepath)}`);
111
+ this._rebuildFile(filepath);
112
+ }
113
+
114
+ _rebuildFile(filepath) {
115
+ const rel = path.relative(this.projectRoot, filepath);
116
+ const moduleId = rel
117
+ .replace(/\.(cpp|cc|mm)$/, '')
118
+ .replace(/[\/\\]/g, '_')
119
+ .replace(/[^a-zA-Z0-9_]/g, '_');
120
+
121
+ const dylibPath = path.join(this.outputDir, `${moduleId}.dylib`);
122
+
123
+ this.building.add(filepath);
124
+ this.emit('build:start', filepath);
125
+ console.log(`[nativ] Building dylib for ${rel}...`);
126
+
127
+ const isObjCpp = filepath.endsWith('.mm');
128
+ const lang = isObjCpp ? 'objective-c++' : 'c++';
129
+
130
+ // Keep sysroot for device compilation, filter for host-only paths
131
+ const hostPaths = this.includePaths.filter((p, i, arr) => {
132
+ if (p === '-isysroot') return false;
133
+ if (i > 0 && arr[i - 1] === '-isysroot') return false;
134
+ return true;
135
+ });
136
+
137
+ // Extract exports
138
+ const { extractCppExports } = require('../extractors/cpp-ast-extractor');
139
+ const exports = extractCppExports(filepath, this.includePaths, null);
140
+
141
+ if (exports.length === 0) {
142
+ console.warn(`[nativ] No NATIV_EXPORT functions in ${rel}, skipping dylib`);
143
+ this.building.delete(filepath);
144
+ this.emit('build:error', filepath, 'No exports found');
145
+ return;
146
+ }
147
+
148
+ const bridgePath = filepath.replace(/\.(cpp|cc|mm)$/, '_bridge_hot.cpp');
149
+ const bridgeSource = this._generateBridge(exports, moduleId);
150
+ fs.writeFileSync(bridgePath, bridgeSource);
151
+
152
+ // Compile source + bridge → dylib
153
+ const cmd = [
154
+ 'clang++',
155
+ '-x', lang,
156
+ '-std=c++17',
157
+ '-arch', 'arm64',
158
+ '-dynamiclib',
159
+ '-fPIC',
160
+ '-isysroot', this.sdkPath,
161
+ ...hostPaths,
162
+ // Link against the app's static lib for the registry symbols
163
+ '-undefined', 'dynamic_lookup',
164
+ '-o', dylibPath,
165
+ filepath,
166
+ bridgePath,
167
+ ];
168
+
169
+ if (isObjCpp) {
170
+ cmd.push('-framework', 'Foundation');
171
+ }
172
+
173
+ exec(cmd.join(' '), { encoding: 'utf8' }, (err, stdout, stderr) => {
174
+ this.building.delete(filepath);
175
+
176
+ // Clean up bridge file
177
+ try { fs.unlinkSync(bridgePath); } catch {}
178
+
179
+ if (err) {
180
+ console.error(`[nativ] Build failed: ${path.basename(filepath)}`);
181
+ console.error(stderr.slice(0, 500));
182
+ this.emit('build:error', filepath, stderr);
183
+ return;
184
+ }
185
+
186
+ // Sign the dylib
187
+ if (this.signingIdentity) {
188
+ try {
189
+ execSync(`codesign -fs "${this.signingIdentity}" "${dylibPath}"`, {
190
+ stdio: 'pipe',
191
+ });
192
+ } catch (signErr) {
193
+ console.warn(`[nativ] Signing failed (will try unsigned): ${signErr.message}`);
194
+ }
195
+ }
196
+
197
+ const size = fs.statSync(dylibPath).size;
198
+ console.log(`[nativ] Built ${moduleId}.dylib (${(size / 1024).toFixed(1)}KB)`);
199
+ this.emit('build:complete', filepath, moduleId, dylibPath);
200
+ });
201
+ }
202
+
203
+ _generateBridge(exports, moduleId) {
204
+ const lines = [
205
+ '// Hot-reload bridge — auto-generated',
206
+ '#include <string>',
207
+ '#include <cstdlib>',
208
+ '',
209
+ '// Forward declarations',
210
+ ];
211
+
212
+ for (const fn of exports) {
213
+ const argTypes = fn.args.map(a => a.type + ' ' + a.name).join(', ');
214
+ lines.push(`extern ${fn.ret} ${fn.name}(${argTypes});`);
215
+ }
216
+
217
+ lines.push('', '// Registry C API', 'extern "C" {');
218
+ lines.push('typedef const char* (*NativSyncFn)(const char*);');
219
+ lines.push('typedef void (*NativAsyncFn)(const char*, void(*)(const char*), void(*)(const char*, const char*));');
220
+ lines.push('void nativ_register_sync(const char*, const char*, NativSyncFn);');
221
+ lines.push('void nativ_register_async(const char*, const char*, NativAsyncFn);');
222
+ lines.push('}');
223
+ lines.push('');
224
+
225
+ // Minimal JSON helpers inline
226
+ lines.push('static double _parseNumber(const char* &p) {');
227
+ lines.push(' while (*p == \' \' || *p == \',\' || *p == \'[\') p++;');
228
+ lines.push(' char* end; double v = strtod(p, &end); p = end; return v;');
229
+ lines.push('}');
230
+ lines.push('static std::string _parseString(const char* &p) {');
231
+ lines.push(' while (*p && *p != \'"\') p++; if (*p == \'"\') p++;');
232
+ lines.push(' std::string s; while (*p && *p != \'"\') { if (*p == \'\\\\\' && *(p+1)) { p++; s += *p; } else { s += *p; } p++; }');
233
+ lines.push(' if (*p == \'"\') p++; return s;');
234
+ lines.push('}');
235
+ lines.push('');
236
+
237
+ lines.push('extern "C" {');
238
+
239
+ for (const fn of exports) {
240
+ const isSync = !fn.async;
241
+ if (isSync) {
242
+ lines.push(`static const char* nativ_cpp_${moduleId}_${fn.name}(const char* argsJson) {`);
243
+ lines.push(' const char* p = argsJson;');
244
+ lines.push(' while (*p && *p != \'[\') p++; if (*p == \'[\') p++;');
245
+
246
+ // Generate arg extraction
247
+ const argNames = [];
248
+ for (const arg of fn.args) {
249
+ const t = arg.type.replace(/const\s+/, '').replace(/\s*&\s*$/, '').trim();
250
+ if (t === 'std::string') {
251
+ lines.push(` std::string ${arg.name} = _parseString(p);`);
252
+ } else {
253
+ lines.push(` ${arg.type} ${arg.name} = (${arg.type})_parseNumber(p);`);
254
+ }
255
+ argNames.push(arg.name);
256
+ }
257
+
258
+ lines.push(` auto result = ${fn.name}(${argNames.join(', ')});`);
259
+
260
+ // Return type handling
261
+ const retBase = fn.ret.replace(/const\s+/, '').replace(/\s*&\s*$/, '').trim();
262
+ if (retBase === 'std::string') {
263
+ lines.push(' static thread_local std::string buf;');
264
+ lines.push(' buf = "\\""; for (char c : result) { if (c == \'"\') buf += "\\\\\\\\\\""; else buf += c; } buf += "\\"";');
265
+ lines.push(' return buf.c_str();');
266
+ } else {
267
+ lines.push(' static thread_local std::string buf;');
268
+ lines.push(' buf = std::to_string(result);');
269
+ lines.push(' return buf.c_str();');
270
+ }
271
+ lines.push('}');
272
+ }
273
+ }
274
+
275
+ // Constructor — re-registers, replacing statically-linked versions
276
+ lines.push('');
277
+ lines.push('__attribute__((constructor))');
278
+ lines.push(`static void nativ_cpp_register_${moduleId}() {`);
279
+ for (const fn of exports) {
280
+ if (!fn.async) {
281
+ lines.push(` nativ_register_sync("${moduleId}", "${fn.name}", nativ_cpp_${moduleId}_${fn.name});`);
282
+ } else {
283
+ // TODO: async bridge
284
+ }
285
+ }
286
+ lines.push('}');
287
+ lines.push('');
288
+ lines.push('} // extern "C"');
289
+
290
+ return lines.join('\n');
291
+ }
292
+
293
+ /**
294
+ * Triggers a build for `filepath` and returns a promise that resolves
295
+ * when the build completes. Called by the Metro transformer so HMR
296
+ * waits until the dylib is ready.
297
+ */
298
+ buildAndWait(filepath) {
299
+ console.log(`[nativ] buildAndWait: ${path.basename(filepath)}`);
300
+ return new Promise((resolve) => {
301
+ const onComplete = (builtPath) => {
302
+ if (builtPath === filepath) {
303
+ this.removeListener('build:complete', onComplete);
304
+ this.removeListener('build:error', onError);
305
+ console.log(`[nativ] buildAndWait resolved (complete): ${path.basename(filepath)}`);
306
+ resolve();
307
+ }
308
+ };
309
+ const onError = (errorPath) => {
310
+ if (errorPath === filepath) {
311
+ this.removeListener('build:complete', onComplete);
312
+ this.removeListener('build:error', onError);
313
+ console.log(`[nativ] buildAndWait resolved (error): ${path.basename(filepath)}`);
314
+ resolve();
315
+ }
316
+ };
317
+ this.on('build:complete', onComplete);
318
+ this.on('build:error', onError);
319
+
320
+ // Trigger the build (skips if already building this file)
321
+ if (!this.building.has(filepath)) {
322
+ this._rebuildFile(filepath);
323
+ } else {
324
+ console.log(`[nativ] buildAndWait: already building, just waiting`);
325
+ }
326
+ });
327
+ }
328
+
329
+ getDylibPath(moduleId) {
330
+ const p = path.join(this.outputDir, `${moduleId}.dylib`);
331
+ return fs.existsSync(p) ? p : null;
332
+ }
333
+
334
+ isBuilding(filepath) {
335
+ return this.building.has(filepath);
336
+ }
337
+
338
+ stop() {
339
+ // fs.watch handles are cleaned up by GC
340
+ this._watcher = null;
341
+ }
342
+ }
343
+
344
+ module.exports = CppDaemon;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Generates TypeScript declaration files (.d.ts) from extracted C++ exports.
3
+ */
4
+
5
+ const { cppTypeToTS } = require("./type-mapper");
6
+
7
+ /**
8
+ * Generate a .d.ts string for a list of exported functions.
9
+ * @param {{ name: string, async: boolean, args: { name: string, type: string }[], ret: string }[]} exports
10
+ * @param {Record<string, string>} customTypes — NATIV_TYPE mappings
11
+ * @returns {string}
12
+ */
13
+ function generateDTS(exports, customTypes = {}) {
14
+ const lines = ["// Auto-generated by react-native-native", ""];
15
+
16
+ for (const fn of exports) {
17
+ const params = fn.args
18
+ .map((a) => `${a.name}: ${cppTypeToTS(a.type, customTypes)}`)
19
+ .join(", ");
20
+ const retType = cppTypeToTS(fn.ret, customTypes);
21
+ const fullRetType = fn.async ? `Promise<${retType}>` : retType;
22
+
23
+ lines.push(
24
+ `export declare function ${fn.name}(${params}): ${fullRetType};`,
25
+ );
26
+ }
27
+
28
+ lines.push("");
29
+ return lines.join("\n");
30
+ }
31
+
32
+ module.exports = { generateDTS };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Resolves C++/ObjC++ include paths for clang invocations outside Xcode.
3
+ * Gathers paths from: iOS SDK, CocoaPods headers, React Native, Xcode build settings.
4
+ * Results are cached per Metro session.
5
+ */
6
+
7
+ const { execSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ let _cached = null;
12
+
13
+ function getIncludePaths(projectRoot) {
14
+ if (_cached) return _cached;
15
+
16
+ const paths = [];
17
+
18
+ // 1. iOS SDK sysroot
19
+ try {
20
+ const sdk = execSync('xcrun --sdk iphonesimulator --show-sdk-path', {
21
+ encoding: 'utf8',
22
+ }).trim();
23
+ paths.push(`-isysroot`, sdk);
24
+ } catch {
25
+ console.warn('[nativ] Could not resolve iOS SDK path');
26
+ }
27
+
28
+ // 2. CocoaPods public headers
29
+ const podsPublic = path.join(projectRoot, 'ios/Pods/Headers/Public');
30
+ if (fs.existsSync(podsPublic)) {
31
+ paths.push(`-I${podsPublic}`);
32
+ }
33
+
34
+ // 3. CocoaPods private headers (for Yoga, React internals)
35
+ const podsPrivate = path.join(projectRoot, 'ios/Pods/Headers/Private');
36
+ if (fs.existsSync(podsPrivate)) {
37
+ paths.push(`-I${podsPrivate}`);
38
+ }
39
+
40
+ // 4. React Native headers
41
+ const rnDirs = [
42
+ 'node_modules/react-native/ReactCommon',
43
+ 'node_modules/react-native/Libraries',
44
+ 'node_modules/react-native/React',
45
+ ];
46
+ for (const dir of rnDirs) {
47
+ const abs = path.join(projectRoot, dir);
48
+ if (fs.existsSync(abs)) {
49
+ paths.push(`-I${abs}`);
50
+ }
51
+ }
52
+
53
+ // 5. Nativ.h — lives in this package's metro/ directory
54
+ const nativHeaderDir = path.resolve(__dirname, '..');
55
+ paths.push(`-I${nativHeaderDir}`);
56
+
57
+ // 6. User source directories
58
+ for (const dir of ['src', 'cpp', 'ios', 'include']) {
59
+ const abs = path.join(projectRoot, dir);
60
+ if (fs.existsSync(abs)) {
61
+ paths.push(`-I${abs}`);
62
+ }
63
+ }
64
+
65
+ _cached = paths;
66
+ return paths;
67
+ }
68
+
69
+ function invalidateCache() {
70
+ _cached = null;
71
+ }
72
+
73
+ module.exports = { getIncludePaths, invalidateCache };