|
| 1 | +--- |
| 2 | +title: Module Interoperability in ClearScript 7.3.7 |
| 3 | +--- |
| 4 | +Standard (ES6) modules can now import CommonJS modules. |
| 5 | + |
| 6 | +# Introduction |
| 7 | + |
| 8 | +[JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) offer a way to split complex scripts into independent functional units with well-defined interfaces. The [`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) and [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) declarations, as well as the [`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) operator, were introduced in ECMAScript 2015 (ES6) as a standard way for modules to share code and data. |
| 9 | + |
| 10 | +This facility supersedes earlier module specifications such as [CommonJS](https://commonjs.org/). However, the latter remains in heavy use, and many popular libraries aren't available in any other form. |
| 11 | + |
| 12 | +ClearScript 7.3.7 allows JavaScript modules to import resources from CommonJS libraries. In this post we'll walk through an example. |
| 13 | + |
| 14 | +# Basic Setup |
| 15 | + |
| 16 | +For this example, let's allow scripts to load documents and use the console: |
| 17 | + |
| 18 | +{% highlight C# %} |
| 19 | + |
| 20 | +engine.DocumentSettings.AccessFlags = DocumentAccessFlags.EnableFileLoading; |
| 21 | +engine.AddHostType(typeof(Console)); |
| 22 | + |
| 23 | +{% endhighlight %} |
| 24 | + |
| 25 | + |
| 26 | +# Document Categories |
| 27 | + |
| 28 | +ClearScript uses [_document categories_](https://microsoft.github.io/ClearScript/Reference/html/T_Microsoft_ClearScript_DocumentCategory.htm) to distinguish between the following document types: |
| 29 | +- JavaScript |
| 30 | +module – [`ModuleCategory.Standard`](https://microsoft.github.io/ClearScript/Reference/html/P_Microsoft_ClearScript_JavaScript_ModuleCategory_Standard.htm) |
| 31 | +- CommonJS module – [`ModuleCategory.CommonJS`](https://microsoft.github.io/ClearScript/Reference/html/P_Microsoft_ClearScript_JavaScript_ModuleCategory_CommonJS.htm) |
| 32 | +- Normal script – [`DocumentCategory.Script`](https://microsoft.github.io/ClearScript/Reference/html/P_Microsoft_ClearScript_DocumentCategory_Script.htm) |
| 33 | + |
| 34 | +However, it has no way to _detect_ the category of a document. Instead, the host must specify the category when it initiates script execution: |
| 35 | + |
| 36 | +{% highlight C# %} |
| 37 | + |
| 38 | +engine.Execute(new DocumentInfo { Category = ModuleCategory.Standard }, @" |
| 39 | + import { Rectangle } from 'Geometry'; |
| 40 | + Console.WriteLine('The area is {0}.', new Rectangle(3, 4).Area); |
| 41 | +"); |
| 42 | + |
| 43 | +{% endhighlight %} |
| 44 | + |
| 45 | +If the host doesn't provide a category, ClearScript assumes `DocumentCategory.Script`. |
| 46 | + |
| 47 | +On the other hand, if a module is loaded on behalf of another module, it _inherits_ its category from the requesting module. In our example, **Geometry** inherits `ModuleCategory.Standard`. |
| 48 | + |
| 49 | +# Overriding the Category |
| 50 | + |
| 51 | +Now let's suppose that **Geometry** is actually a CommonJS module and looks something like this: |
| 52 | + |
| 53 | +{% highlight JavaScript %} |
| 54 | + |
| 55 | +// Geometry.js |
| 56 | +exports.Rectangle = class { |
| 57 | + constructor(width, height) { |
| 58 | + this.width = width; |
| 59 | + this.height = height; |
| 60 | + } |
| 61 | + get Area() { |
| 62 | + return this.width * this.height; |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +{% endhighlight %} |
| 67 | + |
| 68 | +Normally, the sample code above would result in a [`SyntaxError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError) with a message such as "The requested module 'Geometry' does not provide an export named 'Rectangle'". |
| 69 | + |
| 70 | +To allow our example to work, we must _override_ **Geometry**'s document category. To do that, we can use a [document load callback](https://microsoft.github.io/ClearScript/Reference/html/P_Microsoft_ClearScript_DocumentSettings_LoadCallback.htm): |
| 71 | + |
| 72 | +{% highlight C# %} |
| 73 | + |
| 74 | +engine.DocumentSettings.LoadCallback = (ref DocumentInfo info) => { |
| 75 | + if (Path.GetFileNameWithoutExtension(info.Uri.AbsolutePath) == "Geometry") { |
| 76 | + info.Category = ModuleCategory.CommonJS; |
| 77 | + } |
| 78 | +}; |
| 79 | + |
| 80 | +{% endhighlight %} |
| 81 | + |
| 82 | +Note that we're using a simple file name comparison to assign the document category. A real-world host might use a more generic algorithm, basing the assignment on the document's location, its file name extension, an external manifest, or even the document's contents. |
| 83 | + |
| 84 | +# A Final Hurdle |
| 85 | + |
| 86 | +In ClearScript 7.3.7, `V8ScriptEngine` is capable of importing CommonJS resources via the standard `import` declaration and operator. However, by default, the document loader throws an exception if a newly loaded document is of an unexpected category. |
| 87 | + |
| 88 | +In other words, simply overriding the category would make our example fail even earlier – at the document loading stage. ClearScript 7.3.7 retains that behavior for compatibility and safety reasons. In many cases, blocking unexpected document categories is the prudent option. |
| 89 | + |
| 90 | +In _this_ case, however, we can use a new flag to relax that requirement: |
| 91 | + |
| 92 | +{% highlight C# %} |
| 93 | + |
| 94 | +engine.DocumentSettings.AccessFlags |= DocumentAccessFlags.AllowCategoryMismatch; |
| 95 | + |
| 96 | +{% endhighlight %} |
| 97 | + |
| 98 | +With this flag in place, the document loader allows **Geometry** to pass on to the script engine, which now supports the CommonJS module category and safely imports the requested resources. |
| 99 | + |
| 100 | +# Putting It All Together |
| 101 | + |
| 102 | +Here's the complete, working sample code: |
| 103 | + |
| 104 | +{% highlight C# %} |
| 105 | + |
| 106 | +engine.DocumentSettings.AccessFlags = DocumentAccessFlags.EnableFileLoading | DocumentAccessFlags.AllowCategoryMismatch; |
| 107 | +engine.DocumentSettings.LoadCallback = (ref DocumentInfo info) => { |
| 108 | + if (Path.GetFileNameWithoutExtension(info.Uri.AbsolutePath) == "Geometry") { |
| 109 | + info.Category = ModuleCategory.CommonJS; |
| 110 | + } |
| 111 | +}; |
| 112 | +engine.AddHostType(typeof(Console)); |
| 113 | +engine.Execute(new DocumentInfo { Category = ModuleCategory.Standard }, @" |
| 114 | + import { Rectangle } from 'Geometry'; |
| 115 | + Console.WriteLine('The area is {0}.', new Rectangle(3, 4).Area); |
| 116 | +"); |
| 117 | + |
| 118 | +{% endhighlight %} |
| 119 | + |
| 120 | +Module interoperability allows newer scripts to use the standard JavaScript module facility while consuming existing CommonJS libraries. |
| 121 | + |
| 122 | +# How About Reverse Interoperability? |
| 123 | + |
| 124 | +Unfortunately, importing standard modules from CommonJS modules is _not_ possible. The problem has to do with synchronous vs. asynchronous execution modes. |
| 125 | + |
| 126 | +Specifically, standard modules can be [asynchronous](https://github.com/tc39/proposal-top-level-await), so they can invoke both synchronous and asynchronous code at the top level. Even if a module doesn't use [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await), its top-level code can be effectively asynchronous if it imports any asynchronous modules. |
| 127 | + |
| 128 | +The top-level code of a CommonJS module is executed as a normal (synchronous) function, so it cannot interoperate with asynchronous code. |
| 129 | + |
| 130 | +Good luck! |
0 commit comments