nodepyx 1.0.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 (184) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +399 -0
  3. package/binding.gyp +73 -0
  4. package/dist/core/PyCallable.d.ts +65 -0
  5. package/dist/core/PyCallable.d.ts.map +1 -0
  6. package/dist/core/PyCallable.js +109 -0
  7. package/dist/core/PyCallable.js.map +1 -0
  8. package/dist/core/PyContext.d.ts +76 -0
  9. package/dist/core/PyContext.d.ts.map +1 -0
  10. package/dist/core/PyContext.js +228 -0
  11. package/dist/core/PyContext.js.map +1 -0
  12. package/dist/core/PyIterator.d.ts +84 -0
  13. package/dist/core/PyIterator.d.ts.map +1 -0
  14. package/dist/core/PyIterator.js +243 -0
  15. package/dist/core/PyIterator.js.map +1 -0
  16. package/dist/core/PyModule.d.ts +55 -0
  17. package/dist/core/PyModule.d.ts.map +1 -0
  18. package/dist/core/PyModule.js +172 -0
  19. package/dist/core/PyModule.js.map +1 -0
  20. package/dist/core/PyProxy.d.ts +65 -0
  21. package/dist/core/PyProxy.d.ts.map +1 -0
  22. package/dist/core/PyProxy.js +483 -0
  23. package/dist/core/PyProxy.js.map +1 -0
  24. package/dist/core/PyRuntime.d.ts +105 -0
  25. package/dist/core/PyRuntime.d.ts.map +1 -0
  26. package/dist/core/PyRuntime.js +438 -0
  27. package/dist/core/PyRuntime.js.map +1 -0
  28. package/dist/env/CondaManager.d.ts +118 -0
  29. package/dist/env/CondaManager.d.ts.map +1 -0
  30. package/dist/env/CondaManager.js +401 -0
  31. package/dist/env/CondaManager.js.map +1 -0
  32. package/dist/env/PackageInstaller.d.ts +233 -0
  33. package/dist/env/PackageInstaller.d.ts.map +1 -0
  34. package/dist/env/PackageInstaller.js +609 -0
  35. package/dist/env/PackageInstaller.js.map +1 -0
  36. package/dist/env/PythonDetector.d.ts +103 -0
  37. package/dist/env/PythonDetector.d.ts.map +1 -0
  38. package/dist/env/PythonDetector.js +381 -0
  39. package/dist/env/PythonDetector.js.map +1 -0
  40. package/dist/env/VenvManager.d.ts +117 -0
  41. package/dist/env/VenvManager.d.ts.map +1 -0
  42. package/dist/env/VenvManager.js +331 -0
  43. package/dist/env/VenvManager.js.map +1 -0
  44. package/dist/index.d.ts +169 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +393 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/plugins/Plugin.interface.d.ts +41 -0
  49. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  50. package/dist/plugins/Plugin.interface.js +12 -0
  51. package/dist/plugins/Plugin.interface.js.map +1 -0
  52. package/dist/plugins/PluginManager.d.ts +26 -0
  53. package/dist/plugins/PluginManager.d.ts.map +1 -0
  54. package/dist/plugins/PluginManager.js +174 -0
  55. package/dist/plugins/PluginManager.js.map +1 -0
  56. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  57. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  58. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  59. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  60. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  61. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  62. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  63. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  64. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  65. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  66. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  67. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  68. package/dist/plugins/index.d.ts +7 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +12 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  73. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  74. package/dist/serialization/DataFrameBridge.js +355 -0
  75. package/dist/serialization/DataFrameBridge.js.map +1 -0
  76. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  77. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  78. package/dist/serialization/MsgPackSerializer.js +242 -0
  79. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  80. package/dist/serialization/NumpyBridge.d.ts +96 -0
  81. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  82. package/dist/serialization/NumpyBridge.js +323 -0
  83. package/dist/serialization/NumpyBridge.js.map +1 -0
  84. package/dist/serialization/Serializer.d.ts +78 -0
  85. package/dist/serialization/Serializer.d.ts.map +1 -0
  86. package/dist/serialization/Serializer.js +281 -0
  87. package/dist/serialization/Serializer.js.map +1 -0
  88. package/dist/types/PythonTypeMapper.d.ts +87 -0
  89. package/dist/types/PythonTypeMapper.d.ts.map +1 -0
  90. package/dist/types/PythonTypeMapper.js +449 -0
  91. package/dist/types/PythonTypeMapper.js.map +1 -0
  92. package/dist/types/StubCache.d.ts +109 -0
  93. package/dist/types/StubCache.d.ts.map +1 -0
  94. package/dist/types/StubCache.js +333 -0
  95. package/dist/types/StubCache.js.map +1 -0
  96. package/dist/types/TypeGenerator.d.ts +139 -0
  97. package/dist/types/TypeGenerator.d.ts.map +1 -0
  98. package/dist/types/TypeGenerator.js +372 -0
  99. package/dist/types/TypeGenerator.js.map +1 -0
  100. package/dist/types/addon.d.ts +114 -0
  101. package/dist/types/addon.d.ts.map +1 -0
  102. package/dist/types/addon.js +32 -0
  103. package/dist/types/addon.js.map +1 -0
  104. package/dist/types/config.d.ts +175 -0
  105. package/dist/types/config.d.ts.map +1 -0
  106. package/dist/types/config.js +35 -0
  107. package/dist/types/config.js.map +1 -0
  108. package/dist/types/index.d.ts +10 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/index.js +12 -0
  111. package/dist/types/index.js.map +1 -0
  112. package/dist/types/python.d.ts +235 -0
  113. package/dist/types/python.d.ts.map +1 -0
  114. package/dist/types/python.js +7 -0
  115. package/dist/types/python.js.map +1 -0
  116. package/dist/utils/ErrorTranslator.d.ts +83 -0
  117. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  118. package/dist/utils/ErrorTranslator.js +210 -0
  119. package/dist/utils/ErrorTranslator.js.map +1 -0
  120. package/dist/utils/Logger.d.ts +27 -0
  121. package/dist/utils/Logger.d.ts.map +1 -0
  122. package/dist/utils/Logger.js +115 -0
  123. package/dist/utils/Logger.js.map +1 -0
  124. package/dist/utils/MemoryMonitor.d.ts +44 -0
  125. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  126. package/dist/utils/MemoryMonitor.js +143 -0
  127. package/dist/utils/MemoryMonitor.js.map +1 -0
  128. package/package.json +177 -0
  129. package/python/error_handler.py +433 -0
  130. package/python/nodepyx_runtime.py +575 -0
  131. package/python/serializer.py +379 -0
  132. package/python/type_inspector.py +288 -0
  133. package/scripts/build-native.js +68 -0
  134. package/scripts/download-prebuilds.js +99 -0
  135. package/scripts/generate-stubs.js +405 -0
  136. package/scripts/install.js +260 -0
  137. package/src/core/PyCallable.ts +137 -0
  138. package/src/core/PyContext.ts +296 -0
  139. package/src/core/PyIterator.ts +294 -0
  140. package/src/core/PyModule.ts +194 -0
  141. package/src/core/PyProxy.ts +605 -0
  142. package/src/core/PyRuntime.ts +504 -0
  143. package/src/env/CondaManager.ts +451 -0
  144. package/src/env/PackageInstaller.ts +738 -0
  145. package/src/env/PythonDetector.ts +414 -0
  146. package/src/env/VenvManager.ts +396 -0
  147. package/src/index.ts +425 -0
  148. package/src/native/gil_guard.cpp +26 -0
  149. package/src/native/gil_guard.h +175 -0
  150. package/src/native/nodepyx_addon.cpp +886 -0
  151. package/src/native/python_bridge.cpp +790 -0
  152. package/src/native/python_bridge.h +257 -0
  153. package/src/native/thread_pool.cpp +336 -0
  154. package/src/native/thread_pool.h +175 -0
  155. package/src/native/type_converter.cpp +901 -0
  156. package/src/native/type_converter.h +272 -0
  157. package/src/nextjs/PyProvider.tsx +123 -0
  158. package/src/nextjs/index.ts +21 -0
  159. package/src/nextjs/usePython.ts +106 -0
  160. package/src/nextjs/withnodepyx.ts +88 -0
  161. package/src/plugins/Plugin.interface.ts +51 -0
  162. package/src/plugins/PluginManager.ts +155 -0
  163. package/src/plugins/builtin/NumpyPlugin.ts +36 -0
  164. package/src/plugins/builtin/PandasPlugin.ts +49 -0
  165. package/src/plugins/builtin/TorchPlugin.ts +56 -0
  166. package/src/plugins/index.ts +7 -0
  167. package/src/serialization/DataFrameBridge.ts +398 -0
  168. package/src/serialization/MsgPackSerializer.ts +220 -0
  169. package/src/serialization/NumpyBridge.ts +332 -0
  170. package/src/serialization/Serializer.ts +320 -0
  171. package/src/types/PythonTypeMapper.ts +495 -0
  172. package/src/types/StubCache.ts +340 -0
  173. package/src/types/TypeGenerator.ts +491 -0
  174. package/src/types/addon.ts +170 -0
  175. package/src/types/config.ts +226 -0
  176. package/src/types/index.ts +55 -0
  177. package/src/types/python.ts +309 -0
  178. package/src/types/stubs/numpy.d.ts +441 -0
  179. package/src/types/stubs/pandas.d.ts +575 -0
  180. package/src/types/stubs/sklearn.d.ts +728 -0
  181. package/src/types/stubs/torch.d.ts +694 -0
  182. package/src/utils/ErrorTranslator.ts +220 -0
  183. package/src/utils/Logger.ts +119 -0
  184. package/src/utils/MemoryMonitor.ts +175 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * nodepyx — Type Converter Header
3
+ * Converts between Python objects and JavaScript/JSON representations.
4
+ *
5
+ * Conversion strategy:
6
+ * - Primitive types (int, float, bool, str, None) → JSON
7
+ * - Large arrays (numpy, lists) → MessagePack binary
8
+ * - numpy arrays → binary buffer with metadata
9
+ * - pandas DataFrames → Arrow/binary with schema metadata
10
+ * - Complex Python objects → opaque references (PyObjectId)
11
+ */
12
+
13
+ #pragma once
14
+
15
+ #include <Python.h>
16
+ #include <napi.h>
17
+ #include <string>
18
+ #include <vector>
19
+ #include <cstdint>
20
+ #include <optional>
21
+
22
+ namespace nodepyx {
23
+
24
+ /**
25
+ * Wire format identifiers (must match TypeScript SerializedFormat enum)
26
+ */
27
+ enum class SerializedFormat : uint8_t {
28
+ JSON = 0,
29
+ MSGPACK = 1,
30
+ NUMPY_ARRAY = 2,
31
+ PANDAS_DATAFRAME = 3,
32
+ PANDAS_SERIES = 4,
33
+ TORCH_TENSOR = 5,
34
+ PYTHON_REF = 6,
35
+ BYTES = 7,
36
+ NONE = 8,
37
+ };
38
+
39
+ /**
40
+ * Metadata for serialized values.
41
+ */
42
+ struct SerializedMetadata {
43
+ // For NUMPY_ARRAY
44
+ std::string dtype;
45
+ std::vector<int64_t> shape;
46
+ std::vector<int64_t> strides;
47
+ bool isC_contiguous = true;
48
+
49
+ // For PANDAS_DATAFRAME
50
+ std::vector<std::string> columns;
51
+ std::vector<std::string> columnDtypes;
52
+
53
+ // For PYTHON_REF
54
+ uint32_t objectId = 0;
55
+
56
+ // General
57
+ int64_t length = 0;
58
+ int64_t size_bytes = 0;
59
+ };
60
+
61
+ /**
62
+ * A serialized value ready to be sent to JavaScript.
63
+ */
64
+ struct SerializedValue {
65
+ SerializedFormat format = SerializedFormat::NONE;
66
+ std::string jsonData; // For JSON format
67
+ std::vector<uint8_t> binaryData; // For binary formats
68
+ SerializedMetadata metadata;
69
+ };
70
+
71
+ /**
72
+ * TypeConverter — stateless utility class for Python ↔ JavaScript conversions.
73
+ *
74
+ * All methods are static and thread-safe (GIL must be held by caller).
75
+ */
76
+ class TypeConverter {
77
+ public:
78
+ TypeConverter() = delete;
79
+
80
+ // ─── Python → SerializedValue ───────────────────────────────────────────
81
+
82
+ /**
83
+ * Convert any Python object to a SerializedValue for JavaScript consumption.
84
+ * @param obj - Python object (borrowed reference)
85
+ * @param depth - current recursion depth (prevents infinite loops)
86
+ * @returns SerializedValue
87
+ */
88
+ static SerializedValue pythonToSerialized(PyObject* obj, int depth = 0);
89
+
90
+ /**
91
+ * Convert Python primitive to JSON string.
92
+ * Returns std::nullopt if obj is not a primitive.
93
+ */
94
+ static std::optional<std::string> primitiveToJson(PyObject* obj);
95
+
96
+ /**
97
+ * Convert Python list/tuple to JSON array string.
98
+ */
99
+ static std::string listToJson(PyObject* seq, int depth);
100
+
101
+ /**
102
+ * Convert Python dict to JSON object string.
103
+ */
104
+ static std::string dictToJson(PyObject* mapping, int depth);
105
+
106
+ /**
107
+ * Serialize numpy ndarray to binary buffer + metadata.
108
+ */
109
+ static SerializedValue numpyArrayToBuffer(PyObject* array);
110
+
111
+ /**
112
+ * Serialize pandas DataFrame to binary + metadata.
113
+ */
114
+ static SerializedValue dataFrameToBuffer(PyObject* df);
115
+
116
+ /**
117
+ * Serialize pandas Series to binary + metadata.
118
+ */
119
+ static SerializedValue seriesToBuffer(PyObject* series);
120
+
121
+ /**
122
+ * Check if a Python object is a numpy array.
123
+ */
124
+ static bool isNumpyArray(PyObject* obj);
125
+
126
+ /**
127
+ * Check if a Python object is a pandas DataFrame.
128
+ */
129
+ static bool isDataFrame(PyObject* obj);
130
+
131
+ /**
132
+ * Check if a Python object is a pandas Series.
133
+ */
134
+ static bool isSeries(PyObject* obj);
135
+
136
+ /**
137
+ * Check if a Python object is a PyTorch Tensor.
138
+ */
139
+ static bool isTorchTensor(PyObject* obj);
140
+
141
+ // ─── JavaScript/JSON → Python ───────────────────────────────────────────
142
+
143
+ /**
144
+ * Convert a JSON string to a Python object.
145
+ * @returns new Python reference, or nullptr on error
146
+ */
147
+ static PyObject* jsonToPython(const std::string& json);
148
+
149
+ /**
150
+ * Convert a MessagePack binary to a Python object.
151
+ */
152
+ static PyObject* msgpackToPython(const std::vector<uint8_t>& data);
153
+
154
+ /**
155
+ * Convert a typed array buffer to a numpy array.
156
+ */
157
+ static PyObject* bufferToNumpyArray(
158
+ const std::vector<uint8_t>& data,
159
+ const std::string& dtype,
160
+ const std::vector<int64_t>& shape
161
+ );
162
+
163
+ // ─── Type Inspection ────────────────────────────────────────────────────
164
+
165
+ /**
166
+ * Get the Python type name of an object.
167
+ */
168
+ static std::string getTypeName(PyObject* obj);
169
+
170
+ /**
171
+ * Get the full qualified type name (module.class).
172
+ */
173
+ static std::string getQualifiedTypeName(PyObject* obj);
174
+
175
+ /**
176
+ * Check if the object is callable.
177
+ */
178
+ static bool isCallable(PyObject* obj);
179
+
180
+ /**
181
+ * Check if the object is iterable.
182
+ */
183
+ static bool isIterable(PyObject* obj);
184
+
185
+ /**
186
+ * Check if the object is async iterable.
187
+ */
188
+ static bool isAsyncIterable(PyObject* obj);
189
+
190
+ /**
191
+ * Check if the object is a Python module.
192
+ */
193
+ static bool isModule(PyObject* obj);
194
+
195
+ /**
196
+ * Check if the object is a Python class.
197
+ */
198
+ static bool isClass(PyObject* obj);
199
+
200
+ /**
201
+ * Get string representation (repr) of an object.
202
+ */
203
+ static std::string getRepr(PyObject* obj);
204
+
205
+ /**
206
+ * Get string representation (str) of an object.
207
+ */
208
+ static std::string getStr(PyObject* obj);
209
+
210
+ // ─── Error Handling ─────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Capture current Python exception state as strings.
214
+ * Clears the exception after capturing.
215
+ * @returns tuple of (type, message, traceback)
216
+ */
217
+ static std::tuple<std::string, std::string, std::string> captureException();
218
+
219
+ /**
220
+ * Convert a Python exception to JSON for transmission to JS.
221
+ */
222
+ static std::string exceptionToJson();
223
+
224
+ // ─── Object Registry ────────────────────────────────────────────────────
225
+ // Python objects that can't be serialized are stored in a registry
226
+ // and referenced by ID from JavaScript.
227
+
228
+ /**
229
+ * Register a Python object and return its ID.
230
+ * Increments reference count.
231
+ */
232
+ static uint32_t registerObject(PyObject* obj);
233
+
234
+ /**
235
+ * Get a Python object by ID.
236
+ * Returns borrowed reference (do not decref).
237
+ */
238
+ static PyObject* getObject(uint32_t id);
239
+
240
+ /**
241
+ * Increment reference count of a registered object.
242
+ */
243
+ static void incRef(uint32_t id);
244
+
245
+ /**
246
+ * Decrement reference count of a registered object.
247
+ * If refcount reaches 0, removes from registry.
248
+ */
249
+ static void decRef(uint32_t id);
250
+
251
+ /**
252
+ * Get count of registered objects (for monitoring).
253
+ */
254
+ static size_t getRegisteredCount();
255
+
256
+ /**
257
+ * Clear all registered objects (during shutdown).
258
+ */
259
+ static void clearRegistry();
260
+
261
+ private:
262
+ // JSON escape a string
263
+ static std::string jsonEscape(const std::string& s);
264
+
265
+ // Maximum depth for recursive conversion
266
+ static constexpr int MAX_DEPTH = 32;
267
+ // Maximum list/dict size to serialize as JSON
268
+ static constexpr size_t MAX_INLINE_SIZE = 10000;
269
+ };
270
+
271
+ } // namespace nodepyx
272
+
@@ -0,0 +1,123 @@
1
+ /**
2
+ * nodepyx — PyProvider (React Context Provider)
3
+ *
4
+ * Wraps a Next.js / React app to provide the nodepyx runtime to the tree.
5
+ * Initialises nodepyx once on mount and cleans up on unmount.
6
+ *
7
+ * Server Components: nodepyx is only active on the server.
8
+ * Client Components: use usePython() which reads from PyContext.
9
+ *
10
+ * Usage (app/layout.tsx):
11
+ * ─────────────────────────────────────────────────────────────
12
+ * import { PyProvider } from 'nodepyx/next';
13
+ *
14
+ * export default function Layout({ children }) {
15
+ * return (
16
+ * <PyProvider config={{ virtualenv: { path: '.venv', packages: ['pandas'] } }}>
17
+ * {children}
18
+ * </PyProvider>
19
+ * );
20
+ * }
21
+ * ─────────────────────────────────────────────────────────────
22
+ */
23
+
24
+ 'use client';
25
+
26
+ import React, {
27
+ createContext,
28
+ useContext,
29
+ useEffect,
30
+ useState,
31
+ useCallback,
32
+ type ReactNode,
33
+ } from 'react';
34
+ import type { nodepyxConfig } from '../types/config';
35
+
36
+ // ─── Context value ─────────────────────────────────────────────────────────────
37
+
38
+ export interface PyContextValue {
39
+ /** Whether nodepyx has finished initialising. */
40
+ ready: boolean;
41
+ /** Error from initialisation, if any. */
42
+ error: Error | null;
43
+ /** Call a Python function by module.function path and get the result. */
44
+ callPython<T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T>;
45
+ /** Import a Python module proxy. */
46
+ importModule(name: string): Promise<unknown>;
47
+ }
48
+
49
+ const PyContext = createContext<PyContextValue | null>(null);
50
+
51
+ // ─── Provider ──────────────────────────────────────────────────────────────────
52
+
53
+ export interface PyProviderProps {
54
+ children: ReactNode;
55
+ config?: nodepyxConfig;
56
+ /** Suppress console errors on init failure (default: false). */
57
+ silent?: boolean;
58
+ /** Custom loading fallback while nodepyx initialises. */
59
+ fallback?: ReactNode;
60
+ }
61
+
62
+ export function PyProvider({ children, config, silent = false, fallback }: PyProviderProps) {
63
+ const [ready, setReady] = useState(false);
64
+ const [error, setError] = useState<Error | null>(null);
65
+
66
+ useEffect(() => {
67
+ let cancelled = false;
68
+
69
+ (async () => {
70
+ try {
71
+ // Lazy-import to avoid bundling nodepyx on the client
72
+ const nodepyx = await import('../index');
73
+ await nodepyx.init(config);
74
+ if (!cancelled) {setReady(true);}
75
+ } catch (err) {
76
+ if (!silent) {console.error('[nodepyx] PyProvider init error:', err);}
77
+ if (!cancelled) {setError(err instanceof Error ? err : new Error(String(err)));}
78
+ }
79
+ })();
80
+
81
+ return () => {
82
+ cancelled = true;
83
+ import('../index').then((nodepyx) => nodepyx.shutdown()).catch(() => {/* ignore */});
84
+ };
85
+ }, []); // eslint-disable-line -- react-hooks/exhaustive-deps intentionally omitted (empty deps: run once on mount)
86
+
87
+ const callPython = useCallback(async <T = unknown>(
88
+ module: string,
89
+ fn: string,
90
+ ...args: unknown[]
91
+ ): Promise<T> => {
92
+ const nodepyx = await import('../index');
93
+ if (!nodepyx.isInitialized()) {throw new Error('nodepyx is not initialized');}
94
+ const mod = await nodepyx.importModule(module) as Record<string, (...a: unknown[]) => Promise<T>>;
95
+ return mod[fn]!(...args);
96
+ }, []);
97
+
98
+ const importModule = useCallback(async (name: string): Promise<unknown> => {
99
+ const nodepyx = await import('../index');
100
+ return nodepyx.importModule(name);
101
+ }, []);
102
+
103
+ if (!ready && !error && fallback) {return <>{fallback}</>;}
104
+
105
+ return (
106
+ <PyContext.Provider value={{ ready, error, callPython, importModule }}>
107
+ {children}
108
+ </PyContext.Provider>
109
+ );
110
+ }
111
+
112
+ // ─── Hook ───────────────────────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Access the PyProvider context.
116
+ * Must be used inside a <PyProvider> tree.
117
+ */
118
+ export function usePython(): PyContextValue {
119
+ const ctx = useContext(PyContext);
120
+ if (!ctx) {throw new Error('usePython() must be used inside <PyProvider>');}
121
+ return ctx;
122
+ }
123
+
@@ -0,0 +1,21 @@
1
+ /**
2
+ * nodepyx/next — Next.js integration entry point.
3
+ *
4
+ * Import from 'nodepyx/next' in your Next.js project.
5
+ *
6
+ * @example
7
+ * // next.config.js
8
+ * const { withnodepyx } = require('nodepyx/next');
9
+ * module.exports = withnodepyx({});
10
+ *
11
+ * @example
12
+ * // React component
13
+ * import { PyProvider, usePython } from 'nodepyx/next';
14
+ */
15
+
16
+ export { withnodepyx } from './withnodepyx';
17
+ export { PyProvider, usePython as usePythonContext } from './PyProvider';
18
+ export { usePython } from './usePython';
19
+ export type { PyContextValue, PyProviderProps } from './PyProvider';
20
+ export type { PyHandle, UsePythonResult } from './usePython';
21
+
@@ -0,0 +1,106 @@
1
+ /**
2
+ * nodepyx — usePython hook
3
+ *
4
+ * Convenience hook for calling Python functions from React components.
5
+ * Provides loading/error state management, automatic cleanup, and
6
+ * memoised callPython helper.
7
+ *
8
+ * Usage:
9
+ * ─────────────────────────────────────────────────────────────
10
+ * 'use client';
11
+ * import { usePython } from 'nodepyx/next';
12
+ *
13
+ * export function MyComponent() {
14
+ * const { data, loading, error, run } = usePython<number[]>(
15
+ * async (py) => {
16
+ * const np = await py.import('numpy');
17
+ * const arr = await np.arange(10);
18
+ * return arr.tolist();
19
+ * }
20
+ * );
21
+ *
22
+ * return (
23
+ * <div>
24
+ * {loading && <p>Running Python...</p>}
25
+ * {error && <p>Error: {error.message}</p>}
26
+ * {data && <pre>{JSON.stringify(data)}</pre>}
27
+ * <button onClick={run}>Run</button>
28
+ * </div>
29
+ * );
30
+ * }
31
+ * ─────────────────────────────────────────────────────────────
32
+ */
33
+
34
+ 'use client';
35
+
36
+ import { useState, useCallback, useRef } from 'react';
37
+
38
+ // Minimal type for the py object exposed to the callback
39
+ export interface PyHandle {
40
+ import(name: string): Promise<unknown>;
41
+ eval(expression: string): Promise<unknown>;
42
+ exec(code: string): Promise<void>;
43
+ }
44
+
45
+ export interface UsePythonResult<T> {
46
+ data: T | null;
47
+ loading: boolean;
48
+ error: Error | null;
49
+ /** Execute the Python callback. Returns the result or throws. */
50
+ run(): Promise<T>;
51
+ /** Reset state to initial (null, false, null). */
52
+ reset(): void;
53
+ }
54
+
55
+ /**
56
+ * React hook that wraps a Python callback with loading/error state.
57
+ *
58
+ * @param callback - Async function receiving a py handle, returns T
59
+ * @param deps - Dependencies array (re-memoises callback when changed)
60
+ */
61
+ export function usePython<T = unknown>(
62
+ callback: (py: PyHandle) => Promise<T>
63
+ ): UsePythonResult<T> {
64
+ const [data, setData] = useState<T | null>(null);
65
+ const [loading, setLoading] = useState(false);
66
+ const [error, setError] = useState<Error | null>(null);
67
+ const abortRef = useRef(false);
68
+
69
+ const run = useCallback(async (): Promise<T> => {
70
+ abortRef.current = false;
71
+ setLoading(true);
72
+ setError(null);
73
+
74
+ try {
75
+ // Lazy-import so this module is safe to import on the server
76
+ const nodepyx = await import('../index');
77
+
78
+ const handle: PyHandle = {
79
+ import: (name) => nodepyx.importModule(name),
80
+ eval: (expr) => nodepyx.evalPython(expr),
81
+ exec: (code) => nodepyx.execPython(code),
82
+ };
83
+
84
+ const result = await callback(handle);
85
+ if (!abortRef.current) {setData(result);}
86
+ return result;
87
+ } catch (err) {
88
+ const e = err instanceof Error ? err : new Error(String(err));
89
+ if (!abortRef.current) {setError(e);}
90
+ throw e;
91
+ } finally {
92
+ if (!abortRef.current) {setLoading(false);}
93
+ }
94
+ // eslint-disable-next-line -- react-hooks/exhaustive-deps intentionally omitted
95
+ }, [callback]);
96
+
97
+ const reset = useCallback(() => {
98
+ abortRef.current = true;
99
+ setData(null);
100
+ setLoading(false);
101
+ setError(null);
102
+ }, []);
103
+
104
+ return { data, loading, error, run, reset };
105
+ }
106
+
@@ -0,0 +1,88 @@
1
+ /**
2
+ * nodepyx — withnodepyx (Next.js config wrapper)
3
+ *
4
+ * Wraps a Next.js config to:
5
+ * - Exclude the native .node addon from webpack bundling
6
+ * - Add node-loader for .node binary files
7
+ * - Mark nodepyx as a serverExternalPackage
8
+ * - Inject nodepyx_* env vars from nodepyxConfig
9
+ *
10
+ * Usage (next.config.js):
11
+ * ──────────────────────────────────────────────────────────────
12
+ * const { withnodepyx } = require('nodepyx/next');
13
+ *
14
+ * module.exports = withnodepyx({
15
+ * // ...your Next.js config
16
+ * }, {
17
+ * virtualenv: { path: './.venv', packages: ['pandas', 'numpy'], autoInstall: true }
18
+ * });
19
+ * ──────────────────────────────────────────────────────────────
20
+ */
21
+
22
+ import type { nodepyxConfig } from '../types/config';
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ type NextConfig = Record<string, any>;
26
+
27
+ export function withnodepyx(
28
+ nextConfig: NextConfig = {},
29
+ nodepyxConfig?: nodepyxConfig
30
+ ): NextConfig {
31
+ return {
32
+ ...nextConfig,
33
+
34
+ // Prevent webpack from bundling native addon
35
+ serverExternalPackages: [
36
+ 'nodepyx',
37
+ ...(nextConfig.serverExternalPackages ?? []),
38
+ ],
39
+
40
+ webpack(webpackConfig: NextConfig, context: { isServer: boolean }) {
41
+ // Exclude nodepyx from client bundles
42
+ if (!context.isServer) {
43
+ webpackConfig.resolve ??= {};
44
+ webpackConfig.resolve.alias = { ...(webpackConfig.resolve.alias ?? {}), nodepyx: false };
45
+ }
46
+
47
+ // node-loader for .node binaries
48
+ webpackConfig.module ??= { rules: [] };
49
+ webpackConfig.module.rules ??= [];
50
+ webpackConfig.module.rules.push({ test: /\.node$/, use: [{ loader: 'node-loader' }] });
51
+
52
+ // Fallbacks
53
+ webpackConfig.resolve ??= {};
54
+ webpackConfig.resolve.fallback = { ...(webpackConfig.resolve.fallback ?? {}), 'node-gyp-build': false };
55
+
56
+ // Server-side external
57
+ if (context.isServer) {
58
+ const prev = webpackConfig.externals ?? [];
59
+ const nodepyxExt = (
60
+ { request }: { request: string },
61
+ cb: (err: null, result?: string) => void
62
+ ) => {
63
+ if (request === 'nodepyx' || request.startsWith('nodepyx/')) {
64
+ return cb(null, `commonjs ${request}`);
65
+ }
66
+ cb(null);
67
+ };
68
+ webpackConfig.externals = Array.isArray(prev) ? [...prev, nodepyxExt] : [prev, nodepyxExt];
69
+ }
70
+
71
+ return typeof nextConfig.webpack === 'function'
72
+ ? nextConfig.webpack(webpackConfig, context)
73
+ : webpackConfig;
74
+ },
75
+
76
+ env: {
77
+ ...nextConfig.env,
78
+ ...(nodepyxConfig ? {
79
+ nodepyx_VENV_PATH: nodepyxConfig.virtualenv?.path ?? '',
80
+ nodepyx_PACKAGES: (nodepyxConfig.virtualenv?.packages ?? []).join(','),
81
+ nodepyx_AUTO_INSTALL: String(nodepyxConfig.virtualenv?.autoInstall ?? false),
82
+ nodepyx_THREAD_POOL: String(nodepyxConfig.threadPoolSize ?? 4),
83
+ nodepyx_LOG_LEVEL: nodepyxConfig.logLevel ?? 'warn',
84
+ } : {}),
85
+ },
86
+ };
87
+ }
88
+
@@ -0,0 +1,51 @@
1
+ /**
2
+ * nodepyx — Plugin Interface
3
+ *
4
+ * Every nodepyx plugin must implement this interface. Plugins can:
5
+ * - Transform Python object values as they cross the JS boundary
6
+ * - Register custom type mappings
7
+ * - Hook into lifecycle events (init, shutdown)
8
+ * - Add convenience methods to PyProxy instances
9
+ */
10
+
11
+ import type { SerializedValue } from '../types/python';
12
+
13
+ // ─── Public types ─────────────────────────────────────────────────────────────
14
+
15
+ export type PythonTypeName = string;
16
+
17
+ export interface PluginInitContext {
18
+ runtimeVersion: string;
19
+ pythonVersion: { major: number; minor: number; patch: number };
20
+ registerTypeHandler(typeName: PythonTypeName, handler: TypeTransformHandler): void;
21
+ }
22
+
23
+ export type TypeTransformHandler = (
24
+ raw: SerializedValue,
25
+ typeHint: string,
26
+ metadata: Record<string, unknown>
27
+ ) => unknown | null;
28
+
29
+ export interface nodepyxPlugin {
30
+ readonly name: string;
31
+ readonly version: string;
32
+ readonly supportedModules: readonly string[];
33
+ readonly handledTypes?: readonly PythonTypeName[];
34
+ readonly description?: string;
35
+ readonly homepage?: string;
36
+
37
+ onInit?(ctx: PluginInitContext): Promise<void> | void;
38
+ onShutdown?(): Promise<void> | void;
39
+ onModuleImported?(moduleName: string, moduleOid: number): Promise<void> | void;
40
+
41
+ transformResult?: TypeTransformHandler;
42
+ serializeValue?(value: unknown, targetTypeName?: string): SerializedValue | null;
43
+ }
44
+
45
+ export interface PluginRegistration {
46
+ plugin: nodepyxPlugin;
47
+ active: boolean;
48
+ loadedAt: number;
49
+ modulesBound: Set<string>;
50
+ }
51
+