@tgrv/void-cli 1.1.0 → 1.1.1

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/bin/void.js CHANGED
@@ -72,6 +72,14 @@ function ensureDir(dir) {
72
72
  }
73
73
  }
74
74
 
75
+ function toJsFriendlyName(name) {
76
+ let clean = name.includes('/') ? name.split('/').pop() : name;
77
+ clean = clean.replace(/^[^a-zA-Z_$]+|[^a-zA-Z0-9_$]+$/g, '');
78
+ return clean.replace(/[-_]([a-z0-9])/gi, (match, group1) => {
79
+ return group1.toUpperCase();
80
+ });
81
+ }
82
+
75
83
  function copyFolderRecursive(src, dest, replacements = {}) {
76
84
  ensureDir(dest);
77
85
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -100,44 +108,44 @@ function copyFolderRecursive(src, dest, replacements = {}) {
100
108
  }
101
109
  }
102
110
 
103
- function copyNonSourceFiles(src, dest, buildOutputDir, filesConfig, wasmFilename) {
104
- if (!filesConfig || !Array.isArray(filesConfig) || filesConfig.length === 0) {
105
- return;
106
- }
111
+ function matchGlob(filePath, pattern) {
112
+ const normalizedPath = filePath.replace(/\\/g, "/");
113
+ const normalizedPattern = pattern.replace(/\\/g, "/");
107
114
 
108
- function matchGlob(filePath, pattern) {
109
- const normalizedPath = filePath.replace(/\\/g, "/");
110
- const normalizedPattern = pattern.replace(/\\/g, "/");
115
+ if (normalizedPath === normalizedPattern) {
116
+ return true;
117
+ }
118
+ const prefix = normalizedPattern.endsWith('/') ? normalizedPattern : normalizedPattern + '/';
119
+ if (normalizedPath.startsWith(prefix)) {
120
+ return true;
121
+ }
111
122
 
112
- if (normalizedPath === normalizedPattern) {
123
+ const globToRegex = (glob) => {
124
+ return '^' + glob
125
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
126
+ .replace(/\*\*/g, '___DOUBLE_STAR___')
127
+ .replace(/\*/g, '___SINGLE_STAR___')
128
+ .replace(/\?/g, '___QUESTION___')
129
+ .replace(/___DOUBLE_STAR___/g, '.*')
130
+ .replace(/___SINGLE_STAR___/g, '[^/]*')
131
+ .replace(/___QUESTION___/g, '.') + '$';
132
+ };
133
+
134
+ const regex = new RegExp(globToRegex(normalizedPattern));
135
+
136
+ if (!normalizedPattern.includes('/')) {
137
+ const baseName = path.basename(normalizedPath);
138
+ if (regex.test(baseName)) {
113
139
  return true;
114
140
  }
115
- const prefix = normalizedPattern.endsWith('/') ? normalizedPattern : normalizedPattern + '/';
116
- if (normalizedPath.startsWith(prefix)) {
117
- return true;
118
- }
119
-
120
- const globToRegex = (glob) => {
121
- return '^' + glob
122
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
123
- .replace(/\*\*/g, '___DOUBLE_STAR___')
124
- .replace(/\*/g, '___SINGLE_STAR___')
125
- .replace(/\?/g, '___QUESTION___')
126
- .replace(/___DOUBLE_STAR___/g, '.*')
127
- .replace(/___SINGLE_STAR___/g, '[^/]*')
128
- .replace(/___QUESTION___/g, '.') + '$';
129
- };
141
+ }
130
142
 
131
- const regex = new RegExp(globToRegex(normalizedPattern));
132
-
133
- if (!normalizedPattern.includes('/')) {
134
- const baseName = path.basename(normalizedPath);
135
- if (regex.test(baseName)) {
136
- return true;
137
- }
138
- }
143
+ return regex.test(normalizedPath);
144
+ }
139
145
 
140
- return regex.test(normalizedPath);
146
+ function copyNonSourceFiles(src, dest, buildOutputDir, filesConfig, wasmFilename) {
147
+ if (!filesConfig || !Array.isArray(filesConfig) || filesConfig.length === 0) {
148
+ return;
141
149
  }
142
150
 
143
151
  function traverse(currentDir) {
@@ -178,6 +186,62 @@ function copyNonSourceFiles(src, dest, buildOutputDir, filesConfig, wasmFilename
178
186
  traverse(src);
179
187
  }
180
188
 
189
+ function cleanupOutputDirectory(pluginDir, buildOutputDir, filesConfig, wasmFilename, typesFilename) {
190
+ if (!fs.existsSync(buildOutputDir)) return;
191
+
192
+ function traverse(currentDir) {
193
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
194
+ for (const entry of entries) {
195
+ const fullPath = path.join(currentDir, entry.name);
196
+ const relativePath = path.relative(buildOutputDir, fullPath);
197
+
198
+ if (entry.isDirectory()) {
199
+ traverse(fullPath);
200
+ // Clean up empty directories
201
+ if (fs.readdirSync(fullPath).length === 0) {
202
+ try {
203
+ fs.rmdirSync(fullPath);
204
+ } catch (e) {}
205
+ }
206
+ } else {
207
+ // Skip core files
208
+ if (entry.name === "index.js" ||
209
+ entry.name === "package.json" ||
210
+ entry.name === wasmFilename ||
211
+ (typesFilename && entry.name === typesFilename)) {
212
+ continue;
213
+ }
214
+
215
+ // Check if the file still exists in the source plugin root
216
+ const sourcePath = path.join(pluginDir, relativePath);
217
+ if (!fs.existsSync(sourcePath)) {
218
+ try {
219
+ fs.unlinkSync(fullPath);
220
+ } catch (e) {}
221
+ continue;
222
+ }
223
+
224
+ // Check if the file still matches the files configuration
225
+ if (!filesConfig || !Array.isArray(filesConfig) || filesConfig.length === 0) {
226
+ try {
227
+ fs.unlinkSync(fullPath);
228
+ } catch (e) {}
229
+ continue;
230
+ }
231
+
232
+ const matches = filesConfig.some(pattern => matchGlob(relativePath, pattern));
233
+ if (!matches) {
234
+ try {
235
+ fs.unlinkSync(fullPath);
236
+ } catch (e) {}
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ traverse(buildOutputDir);
243
+ }
244
+
181
245
  const args = process.argv.slice(2);
182
246
  const command = args[0];
183
247
 
@@ -198,7 +262,7 @@ ${colors.bold}Usage:${colors.reset}
198
262
  process.exit(0);
199
263
  }
200
264
 
201
- // Build core compilation wrapper function to resolve output directory using void.json manifest
265
+ // Build core compilation wrapper
202
266
  function runBuild(pluginPathArg) {
203
267
  const absolutePluginDir = path.resolve(process.cwd(), pluginPathArg);
204
268
  if (!fs.existsSync(absolutePluginDir)) {
@@ -238,7 +302,8 @@ function runBuild(pluginPathArg) {
238
302
  const pluginType = manifest.type;
239
303
  const buildDir = manifest.buildDir;
240
304
  const buildOutputDir = path.join(absolutePluginDir, buildDir);
241
- const exportName = manifest.export || "plugin";
305
+ const rawExport = manifest.export || "plugin";
306
+ const exportName = toJsFriendlyName(rawExport);
242
307
  const wasmFilename = `${exportName}.wasm`;
243
308
 
244
309
  if (pluginType === "sdk") {
@@ -282,6 +347,45 @@ function runBuild(pluginPathArg) {
282
347
  process.exit(1);
283
348
  }
284
349
  builtWasmPath = outputWasm;
350
+ }
351
+ // C++ plugin compilation
352
+ else if (pluginType === "cpp") {
353
+ console.log(`${info} Running: ${colors.blue}emcmake cmake -B build -DCMAKE_BUILD_TYPE=Release${colors.reset}`);
354
+ const configSuccess = runCommand("emcmake cmake -B build -DCMAKE_BUILD_TYPE=Release", { cwd: absolutePluginDir });
355
+ if (!configSuccess) {
356
+ console.error(`${cross} ${colors.red}Emscripten CMake configuration failed. Make sure emscripten SDK is active.${colors.reset}`);
357
+ process.exit(1);
358
+ }
359
+
360
+ console.log(`${info} Running: ${colors.blue}cmake --build build --config Release${colors.reset}`);
361
+ const buildSuccess = runCommand("cmake --build build --config Release", { cwd: absolutePluginDir });
362
+ if (!buildSuccess) {
363
+ console.error(`${cross} ${colors.red}C++ compilation failed.${colors.reset}`);
364
+ process.exit(1);
365
+ }
366
+
367
+ const buildPath = path.join(absolutePluginDir, "build");
368
+ function findWasm(dir) {
369
+ if (!fs.existsSync(dir)) return null;
370
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
371
+ for (const entry of entries) {
372
+ const fullPath = path.join(dir, entry.name);
373
+ if (entry.isDirectory()) {
374
+ const found = findWasm(fullPath);
375
+ if (found) return found;
376
+ } else if (entry.name.endsWith(".wasm")) {
377
+ return fullPath;
378
+ }
379
+ }
380
+ return null;
381
+ }
382
+
383
+ const wasmFile = findWasm(buildPath);
384
+ if (!wasmFile) {
385
+ console.error(`${cross} ${colors.red}Could not find compiled .wasm file in C++ build directory${colors.reset}`);
386
+ process.exit(1);
387
+ }
388
+ builtWasmPath = wasmFile;
285
389
  } else {
286
390
  console.error(`${cross} ${colors.red}Unsupported plugin type: '${pluginType}' in void.json${colors.reset}`);
287
391
  process.exit(1);
@@ -290,6 +394,18 @@ function runBuild(pluginPathArg) {
290
394
  console.log(`${info} Placing build output to local folder at ${colors.bold}${buildOutputDir}${colors.reset}...`);
291
395
  ensureDir(buildOutputDir);
292
396
 
397
+ // Remove past builds' .wasm files from the build output directory
398
+ if (fs.existsSync(buildOutputDir)) {
399
+ const files = fs.readdirSync(buildOutputDir);
400
+ for (const file of files) {
401
+ if (file.endsWith(".wasm")) {
402
+ try {
403
+ fs.unlinkSync(path.join(buildOutputDir, file));
404
+ } catch (e) {}
405
+ }
406
+ }
407
+ }
408
+
293
409
  // Copy WASM
294
410
  fs.copyFileSync(builtWasmPath, path.join(buildOutputDir, wasmFilename));
295
411
 
@@ -312,6 +428,7 @@ function runBuild(pluginPathArg) {
312
428
  main: "index.js",
313
429
  dependencies: {
314
430
  "@tgrv/void-runtime": "^1.0.0",
431
+ ...(manifest.dependencies || {})
315
432
  },
316
433
  publishConfig: {
317
434
  access: "public"
@@ -334,6 +451,13 @@ function runBuild(pluginPathArg) {
334
451
  existingPkg.types = manifest.types;
335
452
  changed = true;
336
453
  }
454
+ if (manifest.dependencies) {
455
+ existingPkg.dependencies = {
456
+ ...existingPkg.dependencies,
457
+ ...manifest.dependencies
458
+ };
459
+ changed = true;
460
+ }
337
461
  if (changed) {
338
462
  fs.writeFileSync(packageJsonPath, JSON.stringify(existingPkg, null, 2));
339
463
  }
@@ -366,6 +490,7 @@ export default ${exportName};
366
490
  `;
367
491
  fs.writeFileSync(path.join(buildOutputDir, "index.js"), indexJsContent);
368
492
  copyNonSourceFiles(absolutePluginDir, buildOutputDir, buildOutputDir, manifest.files, wasmFilename);
493
+ cleanupOutputDirectory(absolutePluginDir, buildOutputDir, manifest.files, wasmFilename, manifest.types);
369
494
  console.log(`${tick} ${colors.green}Successfully completed compilation & packaging!${colors.reset}`);
370
495
  return buildOutputDir;
371
496
  }
@@ -645,13 +770,13 @@ try {
645
770
  // Prompt for language
646
771
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
647
772
  const rawLanguage = await new Promise((resolve) => {
648
- rl.question(`${info} Select plugin language (go/rust) [default: rust]: `, (ans) => {
773
+ rl.question(`${info} Select plugin language (go/rust/cpp) [default: rust]: `, (ans) => {
649
774
  resolve(ans.trim().toLowerCase() || "rust");
650
775
  });
651
776
  });
652
777
  rl.close();
653
778
 
654
- if (rawLanguage !== "rust" && rawLanguage !== "go") {
779
+ if (rawLanguage !== "rust" && rawLanguage !== "go" && rawLanguage !== "cpp") {
655
780
  console.error(`${cross} ${colors.red}Error: Unsupported language: ${rawLanguage}${colors.reset}`);
656
781
  process.exit(1);
657
782
  }
@@ -673,7 +798,7 @@ try {
673
798
  "\\{\\{type\\}\\}": "rust",
674
799
  "\\{\\{workspace_dir\\}\\}": workspaceDir.replace(/\\/g, "/"),
675
800
  });
676
- } else {
801
+ } else if (rawLanguage === "go") {
677
802
  // Verify Go
678
803
  const goCheck = runCommand("go version");
679
804
  if (!goCheck) {
@@ -690,6 +815,23 @@ try {
690
815
  "\\{\\{type\\}\\}": "go",
691
816
  "\\{\\{workspace_dir\\}\\}": workspaceDir.replace(/\\/g, "/"),
692
817
  });
818
+ } else {
819
+ // Verify CMake
820
+ const cmakeCheck = runCommand("cmake --version");
821
+ if (!cmakeCheck) {
822
+ console.error(`\n${cross} ${colors.red}Error: 'cmake' is not installed on your machine.${colors.reset}`);
823
+ console.error(`${colors.yellow}Please install CMake from https://cmake.org/ first.${colors.reset}\n`);
824
+ process.exit(1);
825
+ }
826
+
827
+ console.log(`${info} Creating C++ plugin template at ${colors.bold}${targetDir}${colors.reset}...`);
828
+ ensureDir(targetDir);
829
+ const templateSrc = path.join(cliRoot, "templates", "cpp");
830
+ copyFolderRecursive(templateSrc, targetDir, {
831
+ "\\{\\{name\\}\\}": folderName,
832
+ "\\{\\{type\\}\\}": "cpp",
833
+ "\\{\\{workspace_dir\\}\\}": workspaceDir.replace(/\\/g, "/"),
834
+ });
693
835
  }
694
836
 
695
837
  console.log(`${tick} ${colors.green}Plugin created successfully under '${pluginPathArg}'!${colors.reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tgrv/void-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "void": "./bin/void.js"
@@ -0,0 +1,27 @@
1
+ cmake_minimum_required(VERSION 3.14)
2
+ project({{name}} LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ # Configure output as a WebAssembly module
8
+ set(CMAKE_EXECUTABLE_SUFFIX ".wasm")
9
+
10
+ # Exclude entry point and export functions explicitly for Emscripten
11
+ if (EMSCRIPTEN)
12
+ set(EXPORT_FUNCTIONS "-sEXPORTED_FUNCTIONS=_void_malloc,_void_free,_void_free_string,_void_invoke,_void_init")
13
+ set(EXPORT_METHODS "-sEXPORTED_RUNTIME_METHODS=ccall,cwrap")
14
+ add_link_options(
15
+ "${EXPORT_FUNCTIONS}"
16
+ "${EXPORT_METHODS}"
17
+ "--no-entry"
18
+ "-sALLOW_MEMORY_GROWTH=1"
19
+ )
20
+ endif()
21
+
22
+ # Reference Void C++ SDK
23
+ add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/@tgrv/void-sdk-cpp" void-sdk-cpp-build)
24
+
25
+
26
+ add_executable({{name}} src/plugin.cpp)
27
+ target_link_libraries({{name}} PRIVATE void-sdk-cpp)
@@ -0,0 +1,57 @@
1
+ # Void C++ Plugin Template
2
+
3
+ A template for building WebAssembly (WASM) plugins for the **Void** framework using C++.
4
+
5
+ ## 🚀 Getting Started
6
+
7
+ > [!IMPORTANT]
8
+ > Before building your plugin, run **`npm install`** (or `npm i`) in this root directory to download the C++ SDK dependency (`@tgrv/void-sdk-cpp`). The `package.json` file in this root folder is only used for local development setup and resolving CMake dependencies; it is **not** the package that gets published. The final publishable package (with its own generated `package.json`) will be built inside your output directory (configured in `void.json` as `buildDir`).
9
+
10
+
11
+ ### 1. Where to Start
12
+ Write your plugin logic inside [src/plugin.cpp](src/plugin.cpp). Define handlers matching the signature:
13
+ ```cpp
14
+ nlohmann::json MyHandler(const void_sdk::ArgsMap& args)
15
+ ```
16
+ And register them inside `init_handlers()`:
17
+ ```cpp
18
+ void init_handlers() {
19
+ void_sdk::register_handler("hello", hello);
20
+ }
21
+ VOID_PLUGIN(init_handlers);
22
+ ```
23
+
24
+ ### 2. Build the Plugin
25
+ Compile your plugin by running the build command in this directory:
26
+ ```bash
27
+ npx void build
28
+ ```
29
+ This runs `emcmake` to configure CMake and compiles the C++ code to a WebAssembly binary.
30
+
31
+ ### 3. Test/Install Locally
32
+ To test the built plugin in a local Void application:
33
+ 1. Initialize your application project: `npx void init`
34
+ 2. Add your compiled local plugin build output folder path:
35
+ ```bash
36
+ npx void add ./path/to/plugin/@void/<plugin-name>
37
+ ```
38
+
39
+ ### 4. Publish
40
+ To build and publish the plugin to the npm registry:
41
+ ```bash
42
+ npx void publish
43
+ ```
44
+
45
+ ---
46
+
47
+ ## ⚙️ Configuration (`void.json`)
48
+
49
+ Your plugin configuration is defined in `void.json`. Here are the available fields:
50
+
51
+ - **`name`**: The package name of your plugin (e.g. `my-plugin`).
52
+ - **`version`**: The current semantic version of the plugin.
53
+ - **`type`**: The compilation target language. Set to `"cpp"`.
54
+ - **`buildDir`**: The target directory where compilation and wrapping assets are generated (e.g. `@void/my-plugin`).
55
+ - **`types`**: Path to the TypeScript declaration file (e.g. `"types.d.ts"`).
56
+ - **`export`**: The name of the JavaScript variable used to load the WASM binary, which is also the default export name (e.g. `"myPlugin"`). **Note:** Ensure you use a JS-friendly name (e.g. alphanumeric/camelCase) to ensure the generated JS code does not contain syntax errors. The CLI will automatically sanitize special characters (like `-`, `_`, or `@scope/`) into camelCase/JS-friendly format for the variable and the generated WASM binary name.
57
+ - **`files`**: An array of glob patterns of non-source files (such as `*.md` or `*.d.ts`) to copy from the plugin root into the build output folder.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "void-cpp-plugin",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "@tgrv/void-runtime": "^1.0.0",
8
+ "@tgrv/void-sdk-cpp": "^1.0.0"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "types": "types.d.ts"
14
+ }
@@ -0,0 +1,17 @@
1
+ #include <void/void.hpp>
2
+
3
+ using json = nlohmann::json;
4
+
5
+ // Define a starter function handler
6
+ json hello(const void_sdk::ArgsMap& args) {
7
+ std::string name = void_sdk::get_string(args, "name");
8
+ return json{{"message", "Hello, " + name + " from C++!"}};
9
+ }
10
+
11
+ // Register function handlers
12
+ void init_handlers() {
13
+ void_sdk::register_handler("hello", hello);
14
+ }
15
+
16
+ // Bind initialization entrypoint
17
+ VOID_PLUGIN(init_handlers);
@@ -0,0 +1,7 @@
1
+ declare module '{{name}}' {
2
+ interface CppPlugin {
3
+ hello(args: { name: string }): { message: string };
4
+ }
5
+ const plugin: CppPlugin;
6
+ export default plugin;
7
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "1.0.0",
4
+ "type": "cpp",
5
+ "buildDir": "@void/{{name}}",
6
+ "types": "types.d.ts",
7
+ "export": "{{name}}",
8
+ "dependencies": {
9
+ "@tgrv/void-sdk-cpp": "^1.0.0"
10
+ },
11
+ "files": [
12
+ "*.d.ts",
13
+ "*.md"
14
+ ]
15
+ }
@@ -48,5 +48,5 @@ Your plugin configuration is defined in `void.json`. Here are the available fiel
48
48
  - **`type`**: The compilation target language. Set to `"go"`.
49
49
  - **`buildDir`**: The target directory where compilation and wrapping assets are generated (e.g. `@void/my-plugin`).
50
50
  - **`types`**: Path to the TypeScript declaration file (e.g. `"types.d.ts"`). During the build process, the CLI validates its existence, copies it to the build output, and automatically sets the `"types"` entry in `package.json` for autocomplete and editor support.
51
- - **`export`**: The name of the JavaScript variable used to load the WASM binary, which is also the default export name (e.g. `"myPlugin"`).
51
+ - **`export`**: The name of the JavaScript variable used to load the WASM binary, which is also the default export name (e.g. `"myPlugin"`). **Note:** Ensure you use a JS-friendly name (e.g. alphanumeric/camelCase) to ensure the generated JS code does not contain syntax errors. The CLI will automatically sanitize special characters (like `-`, `_`, or `@scope/`) into camelCase/JS-friendly format for the variable and the generated WASM binary name.
52
52
  - **`files`**: An array of glob patterns of non-source files (such as `*.md` or `*.d.ts`) to copy from the plugin root into the build output folder.
@@ -40,5 +40,5 @@ Your plugin configuration is defined in `void.json`. Here are the available fiel
40
40
  - **`type`**: The compilation target language. Set to `"rust"`.
41
41
  - **`buildDir`**: The target directory where compilation and wrapping assets are generated (e.g. `@void/my-plugin`).
42
42
  - **`types`**: Path to the TypeScript declaration file (e.g. `"types.d.ts"`). During the build process, the CLI validates its existence, copies it to the build output, and automatically sets the `"types"` entry in `package.json` for autocomplete and editor support.
43
- - **`export`**: The name of the JavaScript variable used to load the WASM binary, which is also the default export name (e.g. `"myPlugin"`).
43
+ - **`export`**: The name of the JavaScript variable used to load the WASM binary, which is also the default export name (e.g. `"myPlugin"`). **Note:** Ensure you use a JS-friendly name (e.g. alphanumeric/camelCase) to ensure the generated JS code does not contain syntax errors. The CLI will automatically sanitize special characters (like `-`, `_`, or `@scope/`) into camelCase/JS-friendly format for the variable and the generated WASM binary name.
44
44
  - **`files`**: An array of glob patterns of non-source files (such as `*.md` or `*.d.ts`) to copy from the plugin root into the build output folder.