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

Support for ESM config files (with "type": "module") #3677

Open
tschaub opened this issue Apr 26, 2021 · 19 comments
Open

Support for ESM config files (with "type": "module") #3677

tschaub opened this issue Apr 26, 2021 · 19 comments

Comments

@tschaub
Copy link

tschaub commented Apr 26, 2021

If a project's package.json has "type": "module", Karma fails to load the config file. I'm wondering if there would be interest in supporting ESM config files in addition to CJS (and the other currently supported flavors).

While this might not be complete or the desired solution, it looks like something like this could add ESM config support: tschaub@99f1a92

@devoto13
Copy link
Collaborator

devoto13 commented May 4, 2021

Yes, we would be happy to accept a PR adding support for the ESM config file.

tschaub@99f1a92 is a good start, but it will need some more work. In particular, we'll need to mention this feature in the documentation and add e2e test to prevent regressions (I think we can use .mjs instead of type: module for the purpose of testing for simplicity).

@npetruzzelli
Copy link
Contributor

@devoto13

If I can free up some time, I will try to tackle this. I started a new job so it has kept me very busy! This seems like a natural extension of my previous 2 contributions.

Notes:

Both static import statements and dynamic import() calls both support CommonJS AND ECMA Modules.

The trick is, we would have to use import() to replace require calls when retrieving the config file path that is passed to us, which makes the use of Promise a requirement.

On top of that, I don't know if import() would work the same way as require with the coffeescript, Livescript, or ts-node packages. See: https://github.com/karma-runner/karma/blob/v6.3.2/lib/config.js#L428-L446

The last time I checked, it didn't work because Node's API for intercepting modules isn't yet stable.

I wouldn't hold your breath waiting for me until you see at least a draft PR. It may be a while before I can start.

@tschaub

If you have a more immediate need, you could consume the JavaScript API immediately.

  1. let myKarmaConfig be the result of passing null as the first argument to karma.config.parseConfig()
    • (set the 2nd and 3rd arguments as needed)
  2. Get your config module (with import())
  3. Get the default export
  4. If the default export is a function, call it, passing myKarmaConfig as the only argument.
    • Is the return value a promise?
      • If yes, wait for it to resolve before continuing
    • NOTE: don't use the return value or promise resolution value as your config. myKarmaConfig was updated as a side effect in your config file.
  5. myKarmaConfig should now be a complete config that you can pass to new karma.Server(myKarmaConfig)
  6. Now instead of karma start my.conf.js, you can use node startKarma.js (or whatever you named the file.)

I have a proof of concept published, but it is more complicated than anything that would be implemented in this repo (I tend to over build things). It was written before Karma natively supported Promises and before exception handling was improved, so the end result would need to change before you use it.

@npetruzzelli
Copy link
Contributor

I wouldn't hold your breath waiting for me until you see at least a draft PR. It may be a while before I can start.

I really need to stop saying this prematurely.

See: #3679

@kruncher
Copy link

kruncher commented Jul 2, 2021

Is there a workaround for this?

@netil
Copy link

netil commented Aug 6, 2021

Karma accepts config files with the .js, .ts and .coffee extensions.

If the project package.json has type:"module", just renaming karma.conf.jskarma.conf.ts will make to read config files where it contains CJS module usages.

@loynoir
Copy link

loynoir commented Dec 2, 2021

~20 lines quick fix

NODE_OPTIONS="$NODE_OPTIONS -r $PWD/hook.cjs" karma start karma.config.mjs
// hook.cjs
const { addHook } = require('pirates')

const revert = addHook(
  (code, _filename) =>
    code
      .replace(/\nfunction parseConfig/, 'async function parseConfig')
      .replace('require(configFilePath)', '(await import(configFilePath)).default'),
  {
    exts: ['.js'],
    ignoreNodeModules: false,
    matcher: filename => filename.endsWith('node_modules/karma/lib/config.js')
  }
);

void revert

@loynoir
Copy link

loynoir commented Dec 2, 2021

Above require hook, force import() config.

So

  • if you are using dialect, change -r your_dialect_require_hook to --loader your_dialect_import_hook, if your dialect support import hook.
  • if you are not using dialect, it should always works, as import(cjs) is always ok

@thw0rted
Copy link

It appears that, even though it's not in the documentation, Karma can already take a config function whose return value is a Promise. That allows you to simply write

// karma.conf.js
module.exports = function(config) {
    return import("./karma.conf.mjs").then(val => config.set(val));
}

// karma.conf.mjs
import fooPlugin from "foo-plugin";

export default function(config) {
  config.set({
    ...,
    plugins: [fooPlugin]
  });
}

I needed this when I updated to Angular 13; why I needed it is kind of an epic tale. I'm using Karma with karma-webpack; and my webpack configuration uses the AngularWebpackPlugin. Previously I imported my webpack config file, which was written in ES module format, via the esm package. As of Angular 13, manual webpack configs now require that you pass their shipped code through Babel (!), configured with a linker plugin. The linker plugin (@angular/compiler-cli/linker/babel) is exposed via the exports field in package.json, which means that you cannot use the esm package, so my old setup is no longer possible.

I figured out the solution above, to use Node's own MJS support to dynamically import the Karma config file, which needs to be able to somehow import the Webpack config file. By writing my actual Karma config in MJS, I can simply write import webpackConfig from "./webpack.config.mjs"; and Node handles the heavy lifting, no Karma patches required. (The above works for me in Node 14, Karma 6.3.9.)

@npetruzzelli
Copy link
Contributor

npetruzzelli commented Dec 17, 2021

// karma.conf.js
module.exports = function(config) {
    return import("./karma.conf.mjs").then(val => config.set(val));
}

This won't quite work, or at least it shouldn't (I admit, I haven't tried it). You are close. The value that import() resolves with will be an object representing the exports of the module.

Long Version

// karma.conf.js
const path = require('path')
const absoluteModulePath = path.resolve('./karma.conf.mjs')

module.exports = function(config) {
    // The default module loader only supports File and HTTP(S) urls. File paths
    // without a protocol is an error.
    return import('file:' + absoluteModulePath).then( function(retrievedModule) {
        let defaultExport
        
        // We are only interested in the default export, so we can keep this simple.
        if (typeof retrievedModule?.default !== 'undefined') {
          // The expectation is that `import()` will be used for JavaScript
          // modules and that the result of this will always have a property named
          // "default" regardless of whether it was a CommonJS module or an
          // ECMAScript module.
          defaultExport = retrievedModule.default
        } else {
          // If module promise details are such that CommonJS modules are not
          // assigned to a "default" property on the module object, then this will
          // handle that.
          defaultExport = retrievedModule
        }
        
        if (typeof defaultExport === 'function') {
          return defaultExport(config)
        }
    })
}

Short version:

// karma.conf.js
const path = require('path')
const absoluteModulePath = path.resolve('./karma.conf.mjs')
module.exports = function(config) {
    return import('file:' + absoluteModulePath).then( function(retrievedModule) {
        const defaultExport = (typeof retrievedModule?.default !== 'undefined') ? retrievedModule.default : retrievedModule        
        if (typeof defaultExport === 'function') {
          return defaultExport(config)
        }
    })
}

If you want to be extra cautious, handle errors (such as if the default export isn't a function).

For import() to work at all, you must be using at least version 11 of Node.js. Karma 6.x must support version 10, so it can't take advantage of this method. Version 12 is the earliest LTS version that supports the method.

https://nodejs.org/docs/latest-v12.x/api/esm.html#esm_import_expressions

@npetruzzelli
Copy link
Contributor

The downside to import(), at least as it currently stands, it that it does not support JSON files (doesn't apply to us here, but I'm mentioning for the curious).

You can work around this by checking the path's extension and using the normal require() to retrieve the file and supply the contents to an immediately executed. If necessary, you can always construct your own require function:

https://nodejs.org/docs/latest-v12.x/api/module.html#module_module_createrequire_filename

@npetruzzelli
Copy link
Contributor

It appears that, even though it's not in the documentation

It is documented in the public API page:
http://karma-runner.github.io/6.3/dev/public-api.html#karmaconfig

Though it may be worth adding a link to it from the config file page, in the Overview section, just after the line that describes the export:

Within the configuration file, the configuration code is put together by setting module.exports to point to a function which accepts one argument: the configuration object.

@npetruzzelli
Copy link
Contributor

PR: #3733

@thw0rted
Copy link

Sorry I missed all the activity, I was already off the clock for the weekend. I forgot to come back after tweaking my setup and post what I actually wound up using:

module.exports = function(config) {
    return import("./karma.conf.mjs").then(mod => {
        mod.default(config);
    });
}

That let me convert my old karma.config.js from module.exports = function(config) { config.set(...) } to karma.config.mjs with:

/** @type {(config: import("karma").Config) => void} */
export default function(config) {
    /** @type {import("karma").ConfigOptions} */
    const karmaOptions = { ... };
    config.set(karmaOptions);
}

Obviously this isn't production-ready to work with any arbitrary config-file syntax supported by Karma, but it allowed me to migrate from the way I was already writing my config (in CJS) to ESM syntax with a tiny bit of boilerplate and very few changes otherwise. I'm not saying this is the right solution for Karma going forward but it should get people watching this issue up and running until official support comes along.

@alexanderby
Copy link

Is there a way to do this without karma.config.parseConfig? E.g. import config from './karma.conf.mjs';?

@alexanderby
Copy link

alexanderby commented Dec 20, 2021

I tried patching parseConfig function in config.js file with

if (typeof configFilePath === 'function') {
  configModule = configFilePath;
}

and it works! basePath or files should be updated if config was in a folder other than root.

@npetruzzelli
Copy link
Contributor

npetruzzelli commented Dec 30, 2021

You only need to use parseConfig if you are using the public API. If you are using the CLI, then returning a promise as illustrated above should be all that is needed.

My examples were lengthy, @thw0rted boiled it down to the most minimal version possible. The only change I would make is to return the result of mod.default(config) in case the function itself returns a promise.

module.exports = function(config) {
    return import("./karma.conf.mjs").then(mod => {
        return mod.default(config);
    });
}

or, since it is a single statement, we can take advantage of syntax built into arrow functions:

module.exports = function(config) {
    return import("./karma.conf.mjs").then(mod => mod.default(config));
}

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#comparing_traditional_functions_to_arrow_functions

@alexanderby
Copy link

@npetruzzelli I need to run a new karma.Server and need to parse a config with options for it.

@npetruzzelli
Copy link
Contributor

Since you are using the public API, I recommend checking out the public API docs: http://karma-runner.github.io/6.3/dev/public-api.html#karmaconfig

When using the public API, you will still need an intermediate file like the one described above:

// karma.conf.js
module.exports = function(config) {
    return import("./karma.conf.mjs").then(mod => mod.default(config));
}

This is because, even if the files for your config and new karma.Server are using ESM, internally, Karma v6 still uses the require method and we can't update this to use dynamic import() until (at least) the next major version of Karma.

prestomation added a commit to aws-samples/amazon-sumerian-hosts that referenced this issue Mar 28, 2022
…amples (#95)

* Add engine-specific 'start' commands for running integration tests/examples.

In the root, you may now run `start-core`, `start-three`, or `start-babylon`.

These all run webpack dev server, but will open relevant tabs for the package you have selected. This is either integration tests, or both integration tests and examples.

More work is still required to integrate the integration test code/pages into webpack so they support hot reload

This change also enables source maps for non-production builds.

I've moved demo-credentials.js to the root of the repo. It is onerous for devs to insert what is likely the same cognito pool many places. I've created a fake API in webpack at /devConfig.json where webpack serves up the cognito pool and it is fetched via the samples. Ultimately I do thing we should serve up everything via webpack and probably HtmlWebpackPlugin

I tried to change this to a module with type: module in package.json. If you are reading this, do not attempt this until Karma 7! It doesn't seem like it can work without some really hacky stuff and I could not get it to work even then: Support for ESM config files (with "type": "module") karma-runner/karma#3677. My workaround is to change demo-credentials.js to be node-style instead of a ESM.

A release build won't build any of the examples. These don't work without webpack server because of file location and our devConfig endpoint, so I think it is confusing/misleading to add these to the build output. They wouldn't work anyway without cognito so I don't think this is so bad, but we need to figure out how to test these in CI and how to pull a sample cognito Id from github secrets/etc
@b4rtaz
Copy link

b4rtaz commented Mar 8, 2023

For me that works:

  1. renamed karma.conf.js -> karma.conf.cjs
  2. then run karma by: karma start karma.conf.cjs

robertknight added a commit to hypothesis/browser-extension that referenced this issue Apr 15, 2024
This reduces the need for variation in eg. linting rules by making all JS files
modules where possible. Note that Karma does not support this. See
karma-runner/karma#3677.
robertknight added a commit to hypothesis/browser-extension that referenced this issue Apr 15, 2024
This reduces the need for variation in eg. linting rules by making all JS files
modules where possible. Note that Karma does not support this. See
karma-runner/karma#3677.
robertknight added a commit to hypothesis/browser-extension that referenced this issue Apr 15, 2024
This reduces the need for variation in eg. linting rules by making all JS files
modules where possible. Note that Karma does not support this. See
karma-runner/karma#3677.
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

No branches or pull requests

9 participants