Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port JavaScript runtime to TypeScript #25

Merged
merged 22 commits into from
Oct 2, 2024

Conversation

Tschrock
Copy link
Contributor

@Tschrock Tschrock commented Dec 3, 2021

This is a continuation of #18, resolving some of the comments there and adding additional types and fixes.

Notable changes from #18:

  • Merged in new changes from master
  • Added types for all members and methods
  • Changed error types to pass the message in the super() call
  • Added jsdoc comments for all method arguments/returns
  • Removed the generated KaitaiStream.js file and added it to the .gitignore
  • Updated .npmignore

Some caveats from that PR still apply:

  • This uses Object.setPrototypeOf to work around potential Error inheritence issues
  • This uses rollup.js as the bundler to get a global fallback for umd modules
  • CI/Tests may need additional changes to build the runtime before running

Along with some additional notes:

  • Not all of the types for method arguments were obvious if the function wasn't directly used or didn't use the argument in a way that narrowed it down. I didn't want to make too many assumptions about them, so in those places I've used looser types based on how the function uses it (for example, an argument that's only used with arr.length and arr[0] I've typed as ArrayLike<number> instead of something more specific like Uint8Array)
  • I've made the assumption that methods/members starting with an underscore (like _trimAlloc()) are intended to be private and left the rest public. It's possible there's other methods that were originally intended to be private but weren't marked.
  • I've left many of the exception arguments typed as any, since it was hard to find examples of them being used and theoretically anything could be passed in.

I've managed to run the kaitai tests with this in a node environment, and the results match the current JS version.

I don't feel 100% confident with this yet - but that's mostly because it wasn't clear what all the types should be. The functionality itself hasn't changed much and should still be solid.

vogelsgesang and others added 10 commits May 21, 2020 00:00
This commit ports the JavaScript runtime to TypeScript.

The main work of this commit is to port the KaitaiStream.js to
KaitaiStream.ts. This was pretty much straight search-and-replace task
to map the previously prototype-based class definition to a TypeScript
class definition.

Only in one case, the mapping was not straigtforward: The
deprecated `readBitsInt` was previously set to the exact same function
instance as `readBitsIntBE`. This is not possible in TypeScript.
Instead, this `readBitsInt` is now implemented as a separate function
which simply forwards all calls to `readBitsIntBE`.

This commit does not yet add types to the class member and function
arguments. I am planning to add strict types in a follow-up commit.

To compile the TypeScript file to a JavaScript file, this commit uses
rollup.js. I would have prefered to directly use the TypeScript
compiler, however the TypeScript compiler's UMD loader is missing the
"global" fallback in case no module loader is available.

Usually, I would put the "Kaitaistream.js" into the .gitignore because
it is a generated file. However, doing so would break the existing
super-project structure, because the `run-javascript` script inside the
`tests` repository does not actually build the JavaScript runtime but
only links it
@generalmimon generalmimon self-requested a review February 12, 2022 13:33
@benzsuankularb
Copy link

Looking forward to this PR. 👀

generalmimon added a commit to kaitai-io/kaitai_struct_compiler that referenced this pull request Sep 28, 2024
Until now, byte array literals translated for JavaScript were of type
`number[]`, as TypeScript would call this type (in pure JS terminology,
it's an `Array` object with numbers). For example, a value instance
defined as `instances/v/value: '[65, 126]'` would be translated as
`this._m_v = [65, 126];` in the generated JS code. However, byte arrays
created in any other way would use the
[`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
type, which is the standard type for byte arrays in JavaScript. Namely
any byte array parsed from stream would be a `Uint8Array` object and
also a *non-literal* byte array like `[123, 246 + 0].as<bytes>` would be
generated as `new Uint8Array([123, 246 + 0])`.

I believe that using a different type for byte array literals than for
byte arrays created in any other way was never intentional. It seems we
simply forgot to override `doByteArrayLiteral` in `JavaScriptTranslator`
(but `doByteArrayNonLiteral` is overridden correctly, which is weird),
so we ended up using the default `BaseTranslator.doByteArrayLiteral`
implementation, which produces the `[65, 126]` syntax.

JavaScript is dynamically typed, so you can treat `number[]` the same as
`Uint8Array` in most cases (e.g. indexing will work the same), but this
discrepancy now causes problems when porting our JavaScript runtime
library to TypeScript (see
kaitai-io/kaitai_struct_javascript_runtime#25),
because you would have to annotate a lot of parameter types as `number[]
| Uint8Array` (which doesn't really make sense). Moreover, I discovered
that generating byte array literals as `number[]` creates a bug in this
case:

```ksy
meta:
  id: bytes_to_s
instances:
  literal:
    value: '[65, 126].to_s("UTF-8")'
```

Before this commit, `[65, 126].to_s("UTF-8")` would be translated as
`KaitaiStream.bytesToStr([65, 126], "UTF-8")` for JavaScript. But our
`bytesToStr` implementation won't actually accept `number[]` for
non-ASCII encodings. See
[`KaitaiStream.js:650-657`](https://github.com/kaitai-io/kaitai_struct_javascript_runtime/blob/2acb0d8b09fde9c95fc77ee3019f6d6b1c73f040/KaitaiStream.js#L650-L657):

```js
KaitaiStream.bytesToStr = function(arr, encoding) {
  if (encoding == null || encoding.toLowerCase() === "ascii") {
    return KaitaiStream.createStringFromArray(arr);
  } else {
    if (typeof TextDecoder === 'function') {
      // we're in the browser that supports TextDecoder
      return (new TextDecoder(encoding)).decode(arr);
    } else {
```

If `TextDecoder` is available (which is the case in a browser or since
Node.js 11), the error when trying to access the `literal` instance
above will look like this in the browser (Google Chrome 129):

```
TypeError: Failed to execute 'decode' on 'TextDecoder': parameter 1 is not of type 'ArrayBuffer'.
    at KaitaiStream.bytesToStr (https://ide.kaitai.io/devel/lib/_npm/kaitai-struct/KaitaiStream.js:656:42)
    at BytesToS.get (eval at initCode (https://ide.kaitai.io/devel/js/v1/kaitaiWorker.js:156:9), <anonymous>:27:38)
    at fetchInstance (https://ide.kaitai.io/devel/js/v1/kaitaiWorker.js:123:20)
    ...
```

... and like this in Node.js 12:

```
TypeError [ERR_INVALID_ARG_TYPE]: The "input" argument must be an instance of ArrayBuffer or ArrayBufferView. Received an instance of Array
    at TextDecoder.decode (internal/encoding.js:412:15)
    at Function.KaitaiStream.bytesToStr (/runtime/javascript/KaitaiStream.js:656:42)
    at BytesToS.get (compiled/javascript/BytesToS.js:26:38)
    at /tests/spec/javascript/test_bytes_to_s.js:8:24
    ...
```

So using `Uint8Array` for all byte arrays will not only be more
consistent, but will also fix this bug.
This fixes the `name` property of class constructors. For example,
`KaitaiStream.ValidationNotEqualError.name` now returns
`'ValidationNotEqualError'`. This is consistent with how the built-in
error types work - for example, `RangeError.name` is `'RangeError'`, as
you would expect.

Previously, when the classes were anonymous,
`KaitaiStream.ValidationNotEqualError.name` returned `'class_4'` (which
was an automatically generated name that the TypeScript compiler used in
the output ES5 code). It's worth noting that error constructors were
anonymous also in our handwritten `KaitaiStream.js` before we migrated
to TypeScript ( `KaitaiStream.ValidationNotEqualError.name` used to be
the empty string `''`), so keeping them anonymous wouldn't really be a
regression, but it's nice to improve it now.

I noticed this issue because we use
[`assert.throws`](https://nodejs.org/docs/latest-v20.x/api/assert.html#assertthrowsfn-error-message)
in KST with arguments as in the "Validate instanceof using constructor:"
example. If this assertion fails, Node.js includes the `name` property
of the constructor in the error message if it's not empty. As a result,
the error message was the following when a `ValidationNotEqualError` was
expected but not thrown:

```
Missing expected exception (class_4).
AssertionError [ERR_ASSERTION]: Missing expected exception (class_4).
    at /tests/helpers/javascript/testHelperThrows.js:12:16
    at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read/context:68:3)
```

Now it is `Missing expected exception (ValidationNotEqualError)`.
The status of `prepublish` scripts is still unclear and confusing to me.
https://docs.npmjs.com/cli/v10/using-npm/scripts#life-cycle-scripts
states "prepublish (DEPRECATED)", then
https://docs.npmjs.com/cli/v10/using-npm/scripts#prepare-and-prepublish
explains how `prepublish` is a confusing name (because it doesn't
actually run with `publish` at all), but then it still recommends "use a
`prepublish` script" for tasks such as "Compiling CoffeeScript source
code into JavaScript". That doesn't sound like it's deprecated to me if
it's still recommended in some situations, so maybe the documentation is
wrong.

I suppose the least surprising option is not to rely on any automatic
"hook" and always require running `npm run build` manually.
Copy link
Member

@generalmimon generalmimon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tschrock Thanks! Let's see if it works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants