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

Inlining tests & more #34

Closed
jamiebuilds opened this issue Jan 29, 2021 · 12 comments
Closed

Inlining tests & more #34

jamiebuilds opened this issue Jan 29, 2021 · 12 comments
Labels
out of scope Feature requests which are out of scope for this proposal

Comments

@jamiebuilds
Copy link
Member

jamiebuilds commented Jan 29, 2021

I promise there is a relevant point at the end of this if you stick with me for a second.

An increasing number of language have a way to write various types of tests inside of source files. For example, in Rust: Demo

// math.rs
pub fn add(a: i16, b: i16) -> i16 {
  a + b
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn add_test() {
    assert_eq!(add(35, 7), 42);
  }
}

Module blocks provide a similar sort of syntax. And you could envision something like this:

// math.js
export function add(a, b) {
  return a + b
}

/** @test */
module {
	import { add } from ".."

	export function addTest() {
    assert(add(35, 7) === 42)
	}
}

module {} is dead code, and (once supported) your average optimizing compiler would remove this code. So you can easily avoid it at runtime.

Most JavaScript testing frameworks are already apply various compile-time transforms today, so they already have all the pieces they need to find these modules blocks and execute them in a test environment.

The biggest limitation of the JS module blocks proposal right now is that you cannot import functions, classes, etc from the parent module.

In Rust:

mod tests {
  use super::*;
  // ...
}

Maybe in JavaScript?

module {
  import { add } from ".." // where ".." is whatever syntax makes most sense
  // ...
}

It's easy to imagine how this could be useful for the web workers use case as well.

export function fibSync(n) {
  if (num === 1) {
    return 0
  } else if (num == 2) {
    return 1
  } else {
    return fibSync(num - 1) + fibSync(num - 2)
  }
}

export async function fibAsync(num) {
  if (num < 40) {
    return fibSync(num)
  } else {
    return await worker({ num }, module {
      import { fibSync } from ".."

      export default function ({ num }) {
        return fibSync(self.data.num)
      }
    })
  }
}

Note: I would expect the parent module to evaluate twice for the different environments.

I know this isn't the sort of thing TC39 normally considers when designing features. But I hope this doesn't just get dismissed as a tooling problem. There are way too many of those, and the result ends up being the same syntax with different behaviors depending on the tool you're using (see existing module behavior in webpack, jest, etc.).

@surma
Copy link
Member

surma commented Jan 29, 2021

Interesting point. While not ideal, you can solve this with dynamic import:

export function fib(n) { /* ... */ }

/** @test */
module {
  import {expect} from "chai"
  const {fib} = await import(import.meta.url);

  expect(fib(4)).to.equal(24);
}

I think thee “JavaScript Bundles” proposal that is mentioned in the README might help here. It’s likely out of scope for Module Blocks directly.

@ljharb
Copy link
Member

ljharb commented Jan 30, 2021

That would only be possible if import.meta.url is the URL of the containing document (#26) - but i do like the idea that a module block could import * as ns from parent; or something.

@Jack-Works
Copy link
Member

I like the idea of inlining test modules, but import * as ns from parent does not look good to me

@ljharb
Copy link
Member

ljharb commented Jan 30, 2021

We could pick any non-string specifier :-) the entire design space is open.

@littledan
Copy link
Member

I think this is an interesting use case, but it'd be best if we had a way to address the test module from the outside, so it can be imported by the test framework from the outside, rather than relegating it to dead code, requiring transpilation based on magic comments to make it work. I have some ideas about this, and hope to get back soon with a proposal (more closely related to "JS module bundles" rather than "JS module blocks").

@jamiebuilds
Copy link
Member Author

jamiebuilds commented Feb 1, 2021

@littledan to draw inspiration from TypeScript's import type syntax, do you mean something like this:

// myModule.js
export type Foo = { ... } // example from ts syntax
export test myModuleTests = module { ... }

// myOtherModule.js
import type { Foo } from "myModule.js" // example from ts syntax
import test { myModuleTests } from "myModule.js"

Where type imports are used at type-checking-time, stripped out before runtime, and are statically restricted from being used in non-type positions.

So could test imports be used at "testing-time", stripped out before runtime, and be statically restricted from being used in "non-test" positions, aka:

// myModule.js
export test myModuleTests = module {
  export function foo() {}
}

let newName = myModuleTests // Error: Cannot reference myModuleTests in non-test position
let exports = import(myModuleTests) // Error: Cannot reference myModuleTests in non-test position

// myOtherModule.js
import { myModuleTests } from "myModule.js" // Error: Cannot reference myModuleTests in non-test position
import test { myModuleTests } from "myModule.js" // (No Error)

@Jack-Works
Copy link
Member

I don't like a syntax level solution for this problem (the import test and export test). Unless we made it a more general form

@littledan
Copy link
Member

littledan commented Feb 3, 2021

I wrote out a JS module fragments proposal, where testing could be done like this:

// math.js
export function add(a, b) {
  return a + b
}

module "#test" {
  import { add } from "./math.js" // Hmm, maybe there should be shorthand for this, indeed

  export function addTest() {
    assert(add(35, 7) === 42)
  }
}

Then, the test framework can import math.js#test and do the appropriate thing with it, with no special transpilation needed.

@littledan littledan added the out of scope Feature requests which are out of scope for this proposal label Feb 4, 2021
@Jack-Works
Copy link
Member

I received the email notifying me of someone comments this:

Perhaps we could add a proposal for something like import.meta.super which contains information of the parent module and allow it as a URL specifier during imports:

But I cannot find that comment in the thread. I think this idea is cool to resolve the import.meta.url problem. Maybe import.meta.superUrl

@ljharb
Copy link
Member

ljharb commented Feb 8, 2021

There’s no single concept of a “parent module”, because 0, 1, or 57 modules might import the one in question.

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Feb 8, 2021

I think by "parent module of X" they don't mean "the module which imports X", but "the module inside of which X is declared".

However, we could still have 57 nested module blocks.

@nicolo-ribaudo
Copy link
Member

This proposal doesn't have the goal to extend the import syntax: module expressions can only use the existing import { ... } from "string specifier" declaration format.

However, https://github.com/tc39/proposal-built-in-modules does attempt to expand the import syntax to allow referring to "syntactic modules", and we could consider a "import from the parent" feature as part of that proposal. I opened tc39/proposal-module-declarations#20 to keep track of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
out of scope Feature requests which are out of scope for this proposal
Projects
None yet
Development

No branches or pull requests

6 participants