JeringTech / Javascript.NodeJS

Invoke Javascript in NodeJS, from C#
Other
455 stars 43 forks source link

InvokeFromStringAsync can't find module exports assignment... #141

Closed Jerrill closed 2 years ago

Jerrill commented 2 years ago

I was trying this out for the first time today and got it to kind-of work!

Here is the line of code:

Result? r = await _node.InvokeFromStringAsync<Result>(@"'/node/@shelf/fast-chunk-string/lib/index.js'", args: new object[] { "unicorns", new object[] { 2, false } });

Here is my Result class:

public class Result
{
    public string[]? Tokens { get; set; }
}

I received the following exception:

Jering.Javascript.NodeJS.InvocationException
  HResult=0x80131500
  Message=The module "'/node/@shelf/fast-chunk-..." has no exports. Ensure that the module assigns a function or an object containing functions to module.exports.
  Source=Jering.Javascript.NodeJS
  StackTrace:
   at Jering.Javascript.NodeJS.HttpNodeJSService.<TryInvokeAsync>d__10 1.MoveNext()

packages.json contents:

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "@syncfusion/ej2-js-es5": "20.1.47",
    "runes": "0.4.3",
    "string-length": "4.0.2",
    "@shelf/fast-chunk-string": "3.0.0"
  }
}

index.js contents from the fast-chunk-string module:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = fastChunkString;

var _runes = _interopRequireDefault(require("runes"));

var _stringLength = _interopRequireDefault(require("string-length"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function fastChunkString(str, {
  size,
  unicodeAware = false
}) {
  str = str || '';

  if (!unicodeAware) {
    return getChunks(str, size);
  }

  return getChunksUnicode(str, size);
}

function getChunks(str, size) {
  const strLength = str.length;
  const numChunks = Math.ceil(strLength / size);
  const chunks = new Array(numChunks);
  let i = 0;
  let o = 0;

  for (; i < numChunks; ++i, o += size) {
    chunks[i] = str.substr(o, size);
  }

  return chunks;
}

function getChunksUnicode(str, size) {
  const strLength = (0, _stringLength.default)(str);
  const numChunks = Math.ceil(strLength / size);
  const chunks = new Array(numChunks);
  let i = 0;
  let o = 0;
  const runeChars = (0, _runes.default)(str);

  for (; i < numChunks; ++i, o += size) {
    chunks[i] = runeChars.slice(o, o + size).join('');
  }

  return chunks;
}

module.exports = fastChunkString;

This is first time I'm trying this out so I'm sure I'm missing something. Thoughts?

Thank you! Jerrill

JeremyTCD commented 2 years ago

InvokeFromStringAsync takes raw JS as its first argument. You can call InvokeFromFileAsync to invoke from a file on disk.

The file must export a function with a callback parameter or an object containing several such functions. This means you need a custom "interop" file that:

Example interop file for the library Highlight.js.

Let me know if you encounter any issues.

Jerrill commented 2 years ago

@JeremyTCD Thank you for your help! I was finally able to get it working.

npm was also downloading the modules outside my wwwroot folder so I needed to copy the important pieces using the following batch script that is run as a pre-build action:

node_modules_copy.cmd (created in the project folder)

@echo 'node_modules_copy.cmd' is being executed out of the following folder:
cd
call npm i
pushd node_modules
if exist ..\wwwroot\node_modules (
  rmdir /S /Q ..\wwwroot\node_modules
)
mkdir ..\wwwroot\node_modules
xcopy *.js ..\wwwroot\node_modules /S /Y
xcopy *.css ..\wwwroot\node_modules /S /Y
xcopy *.json ..\wwwroot\node_modules /S /Y
popd
@echo 'node_modules_copy.cmd' completed execution successfully!

added the following to the .csproj file

<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
  <Exec Command="$(ProjectDir)node_modules_copy.cmd" />
</Target>

NOTE: I'm sure there is a "proper" way to do the above, but I haven't found a less complicated method yet...

The particular function I'm testing with returns an array of strings so I was able to get it working with the following code:

string wrapper = @"
    module.exports = (callback, str, size, unicodeAware) => { 
        const fastChunkString = require('@shelf/fast-chunk-string'); 
        var result = fastChunkString(str, {size: size, unicodeAware: unicodeAware});
        callback(null /* error */, result);
    }";
var tokens = await _node.InvokeFromStringAsync<string[]>(wrapper, args: new object[] { "unicorns", 2, false });

after injecting the INodeJSService service as follows: (included for completeness for future lost souls)

private readonly ILogger<HomeController> _logger;
private readonly INodeJSService _node;

public HomeController(ILogger<HomeController> logger, INodeJSService node)
{
    _logger = logger;
    _node = node;
}

Thanks again for your assistance! I'm up and running! Jerrill

Jerrill commented 2 years ago

Ok... I've come to the next question...

Say I have the following existing JS function in my page:

function chunkOutput(size) {
    try {

        if (unchunkedOutput == null) {
             console.log('unchunkedOutput is null.');
             return 0;
        }

        const strLength = unchunkedOutput.length;
        const numChunks = Math.ceil(strLength / size);
        const chunks = new Array(numChunks);

        let i = 0;
        let o = 0;
        for (; i < numChunks; ++i, o += size) {
            chunks[i] = unchunkedOutput.substr(o, size);
        }

        chunkCount = numChunks;
        chunkedOutput = chunks;

        unchunkedOutput = null;

        return numChunks;
    }
    catch (err) {
        console.error('could not chunk the string', err);
    }
}

And it is called successfully from my -->Blazor<-- C# code like this using JSInterop:

int chunkCount = await _jsRuntime.InvokeAsync<int>("chunkOutput", new object?[] { chunkSize });

I would naively try this following direct replacement with Javascript.NodeJS:

string wrapper = @"
    module.exports = (callback, size, unicodeAware) => { 
    const fastChunkString = require('@shelf/fast-chunk-string'); 
    chunkedOutput = fastChunkString(unchunkedOutput, {size: size, unicodeAware: unicodeAware});
    chunkCount = chunkedOutput.length;
    callback(null /* error */, chunkCount);
}";
int chunkCount = await _nodeJs.InvokeFromStringAsync<int>(wrapper, args: new object[] { chunkSize, false });

I would then get the following exception: ;-)

Jering.Javascript.NodeJS.InvocationException: 'unchunkedOutput is not defined
ReferenceError: unchunkedOutput is not defined
    at module.exports (anonymous:4:37)
    at Server.<anonymous> ([eval]:1:2443)
    at Generator.next (<anonymous>)
    at [eval]:1:355
    at new Promise (<anonymous>)
    at o ([eval]:1:100)
    at IncomingMessage.<anonymous> ([eval]:1:557)
    at IncomingMessage.emit (node:events:527:28)
    at endReadableNT (node:internal/streams/readable:1344:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21)'

So accessing the JavaScript from the existing page isn't possible (or maybe isn't possible over a Blazor circuit)?

Also, let's me know if you want me to move this question to a different thread.

Thanks. Jerrill

Jerrill commented 2 years ago

I just found that JavaScript.NodeJS is able to function while the a Razor page is being statically rendered whereas JSInterop is not. So, it may not be a scope issue (i.e. needing to do something like global.chunkCount and adding var global = window; to the non-NodeJS JS to get things to work -- it doesn't, btw!), but rather they are actually running in completely different sandboxes. Thanks for any clarification you can provide.

Jerrill

UPDATE: Thinking this through, where I'm injecting this, it is running on the server side in Blazor. So, I guess the right question (before I venture into Blazor WebAssembly) would be "Can I interop with browser JS variables from this library in Blazor WebAssembly?"

Thanks again.

JeremyTCD commented 2 years ago

No problem, we can discuss here.

but rather they are actually running in completely different sandboxes.

You're right. This library spins up its own Node.js process(es), so it's not interoperable with JS executing elsewhere.

Thinking this through, where I'm injecting this, it is running on the server side in Blazor. So, I guess the right question (before I venture into Blazor WebAssembly) would be "Can I interop with browser JS variables from this library in Blazor WebAssembly?"

From my limited knowledge of Blazer, I do not think this library will work with Blazor WebAssembly - it most likely is not possible to start a Node.js process from a client (browser), plus client machines may not have Node.js installed.

A possible alternative: if the NPM libraries you are using do not require Node.js, you can use webpack to pack them into a single .js file at build time, then Blazor might be able to execute it directly (no need for this library).

An example use of webpack in a .Net project: Web.SyntaxHighlighters.HighlightJS. Files of interest:

Jerrill commented 2 years ago

Cool! Thank you for the confirmation.

I suppose it would be possible to write a server-side process incorporating both JavaScript.NodeJS and JSInterop in order to sync client-side and NodeJS/server-side variables in the background for something that doesn't need a lot of scalability. That would be kind of cool, but it's definitely getting into Rube Goldberg machine territory. I've been trying to come up with a scenario that would make that complexity worth it, but I'm running up against my limited knowledge about what's really available in NodeJS vs just using Nuget packages in C#.

I ultimately found https://www.snowpack.dev/ that compiles NodeJS modules down to ES modules that just work browser/client-side. And then there is https://www.skypack.dev/ which makes ES modules pre-built from NodeJS modules (I think using snowpack) available via cdn. And it's the skypack modules that I've decided to run with for development for now.

Thank you for your help and support along the way and for having patience with a newbie's NodeJS journey!

No issue with JavaScript.NodeJS. It works well!

pebcak

Jerrill

JeremyTCD commented 2 years ago

Happy to help, appreciate your sharing what you've found. Will close this issue for now, feel free to open a new issue if you have any other questions about this library.

All the best with your Node.js journey!