Repository: karma-runner/karma (js)

Issue:
Load scoped package plugins by default
### Expected behavior
Karma should be able to load plugins with `@*/karma-*` in their name.

### Actual behavior
Karma will only load plugins with names that start with `karma-*`.
It's incompatible with the [scoped](https://docs.npmjs.com/cli/v7/using-npm/scope) package plugins.
[When finding plugins, Karma doesn't care about subdirectories](https://github.com/karma-runner/karma/blob/99908c3f5b5e48bc2e2158d090ca2e9d514fcb03/lib/plugin.js#L39), so even if you set the `plugins` pattern to like `@*/karma-*`, it will not match anything.

Pull request description:
feat(plugins): add support wildcard config for scoped package plugin
If you wish load all scoped package plugins by `@*/karma-*` like `karma-*`, karma doesn't handle it.
It'll be modified to `RegExp("^@.*/karma-*")` internally, and karma doesn't care scoped package's sub directories.

In this PR, add features below.

- support plugin name with multiple `*` (replace ALL `*` to `.*`)
  - `@*/karma-*` is now valid
  - `karma-*` also work same as before
  - **however, if anyone already force using multiple `*` (such as `karma-*-foo-.*`), there is a risk of incompatibility**
- when finding plugin, get scoped package name now correctly (before: `@foo`, now: `@foo/bar`)

Fixes #3656

Patch:
diff --git a/.travis.yml b/.travis.yml
index 7c1e548d..e6bee9ac 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,7 +19,9 @@ jobs:
     - node_js: "14"
       script:
         - npm run init
-        - commitlint-travis
+        - if [[ "$TRAVIS_BRANCH" != "master" ]]; then
+            commitlint-travis;
+          fi;
         - npm run lint
         - npm run build:check
         - npm run test:unit
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea0f2606..e614239e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## [6.1.2](https://github.com/karma-runner/karma/compare/v6.1.1...v6.1.2) (2021-03-09)
+
+
+### Bug Fixes
+
+* **commitlint:** skip task on master ([#3650](https://github.com/karma-runner/karma/issues/3650)) ([3fc6fda](https://github.com/karma-runner/karma/commit/3fc6fdadd6b0ed6838de048c15485b1bd815fe23))
+* patch karma to allow loading virtual packages ([#3663](https://github.com/karma-runner/karma/issues/3663)) ([5bfcf5f](https://github.com/karma-runner/karma/commit/5bfcf5f37de6f0a12abcf9914c2fad510395b4d6))
+
 ## [6.1.1](https://github.com/karma-runner/karma/compare/v6.1.0...v6.1.1) (2021-02-12)
 
 
diff --git a/lib/middleware/karma.js b/lib/middleware/karma.js
index 3a34b4e5..37e9a46c 100644
--- a/lib/middleware/karma.js
+++ b/lib/middleware/karma.js
@@ -222,10 +222,10 @@ function createKarmaMiddleware (
           }) : []
 
           return data
-            .replace('%SCRIPTS%', scriptTags.join('\n'))
+            .replace('%SCRIPTS%', () => scriptTags.join('\n'))
             .replace('%CLIENT_CONFIG%', 'window.__karma__.config = ' + JSON.stringify(client) + ';\n')
-            .replace('%SCRIPT_URL_ARRAY%', 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n')
-            .replace('%MAPPINGS%', 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n')
+            .replace('%SCRIPT_URL_ARRAY%', () => 'window.__karma__.scriptUrls = ' + JSON.stringify(scriptUrls) + ';\n')
+            .replace('%MAPPINGS%', () => 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n')
             .replace('\n%X_UA_COMPATIBLE%', getXUACompatibleMetaElement(request.url))
         })
       })
diff --git a/lib/plugin.js b/lib/plugin.js
index 53dfc68a..8805a288 100644
--- a/lib/plugin.js
+++ b/lib/plugin.js
@@ -32,12 +32,21 @@ function resolve (plugins, emitter) {
         return
       }
       const pluginDirectory = path.normalize(path.join(__dirname, '/../..'))
-      const regexp = new RegExp(`^${plugin.replace('*', '.*')}`)
+      const regexp = new RegExp(`^${plugin.replace(/\*/g, '.*').replace(/\//g, '[/\\\\]')}`)
 
       log.debug(`Loading ${plugin} from ${pluginDirectory}`)
       fs.readdirSync(pluginDirectory)
-        .filter((pluginName) => !IGNORED_PACKAGES.includes(pluginName) && regexp.test(pluginName))
-        .forEach((pluginName) => requirePlugin(`${pluginDirectory}/${pluginName}`))
+        .map((e) => {
+          const modulePath = path.join(pluginDirectory, e)
+          if (e[0] === '@') {
+            return fs.readdirSync(modulePath).map((e) => path.join(modulePath, e))
+          }
+          return modulePath
+        })
+        .reduce((a, x) => a.concat(x), [])
+        .map((modulePath) => path.relative(pluginDirectory, modulePath))
+        .filter((moduleName) => !IGNORED_PACKAGES.includes(moduleName) && regexp.test(moduleName))
+        .forEach((pluginName) => requirePlugin(path.join(pluginDirectory, pluginName)))
     } else if (helper.isObject(plugin)) {
       log.debug(`Loading inline plugin defining ${Object.keys(plugin).join(', ')}.`)
       modules.push(plugin)
diff --git a/package-lock.json b/package-lock.json
index 7e44ef44..9bcac86e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "karma",
-  "version": "6.1.1",
+  "version": "6.1.2",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 93f23a5d..95562fd7 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
     "Bryan Smith <bryanrsmith@gmail.com>",
     "Bulat Shakirzyanov <mallluhuct@gmail.com>",
     "ChangZhuo Chen (陳昌倬) <czchen@gmail.com>",
+    "Chris Bottin <chrisbottin@users.noreply.github.com>",
     "Cyrus Chan <chan1cyrus2@gmail.com>",
     "DarthCharles <carlos.darth@gmail.com>",
     "David Herges <dherges@users.noreply.github.com>",
@@ -172,9 +173,9 @@
     "Carl Goldberg <carlgoldberg1236@gmail.com>",
     "Chad Smith <chad@configit.com>",
     "Chang Wang <cheapsteak@gmail.com>",
+    "Charles Suh <charlessuh@users.noreply.github.com>",
     "Chelsea Urquhart <curquhart@users.noreply.github.com>",
     "Chris <camargo.cac@gmail.com>",
-    "Chris Bottin <chrisbottin@users.noreply.github.com>",
     "Chris Chua <chris.sirhc@gmail.com>",
     "Chris Dawson <xrdawson@gmail.com>",
     "Christian Weiss <cweiss@stripe.com>",
@@ -487,7 +488,7 @@
   "engines": {
     "node": ">= 10"
   },
-  "version": "6.1.1",
+  "version": "6.1.2",
   "license": "MIT",
   "husky": {
     "hooks": {


Test patch:
diff --git a/test/unit/middleware/karma.spec.js b/test/unit/middleware/karma.spec.js
index 8a968262..b6a0d5d5 100644
--- a/test/unit/middleware/karma.spec.js
+++ b/test/unit/middleware/karma.spec.js
@@ -28,6 +28,7 @@ describe('middleware.karma', () => {
     karma: {
       static: {
         'client.html': mocks.fs.file(0, 'CLIENT HTML\n%X_UA_COMPATIBLE%%X_UA_COMPATIBLE_URL%'),
+        'client_with_context.html': mocks.fs.file(0, 'CLIENT_WITH_CONTEXT\n%SCRIPT_URL_ARRAY%'),
         'context.html': mocks.fs.file(0, 'CONTEXT\n%SCRIPTS%'),
         'debug.html': mocks.fs.file(0, 'DEBUG\n%SCRIPTS%\n%X_UA_COMPATIBLE%'),
         'karma.js': mocks.fs.file(0, 'root: %KARMA_URL_ROOT%, proxy: %KARMA_PROXY_PATH%, v: %KARMA_VERSION%')
@@ -214,6 +215,21 @@ describe('middleware.karma', () => {
     callHandlerWith('/__karma__/context.html')
   })
 
+  it('should serve context.html without using special patterns when replacing script tags', (done) => {
+    includedFiles([
+      new MockFile('/.yarn/$$virtual/first.js', 'sha123'),
+      new MockFile('/.yarn/$$virtual/second.dart', 'sha456')
+    ])
+
+    response.once('end', () => {
+      expect(nextSpy).not.to.have.been.called
+      expect(response).to.beServedAs(200, 'CONTEXT\n<script type="text/javascript" src="/__proxy__/__karma__/absolute/.yarn/$$virtual/first.js?sha123" crossorigin="anonymous"></script>\n<script type="text/javascript" src="/__proxy__/__karma__/absolute/.yarn/$$virtual/second.dart?sha456" crossorigin="anonymous"></script>')
+      done()
+    })
+
+    callHandlerWith('/__karma__/context.html')
+  })
+
   it('should serve context.html with replaced link tags', (done) => {
     includedFiles([
       new MockFile('/first.css', 'sha007'),
@@ -373,6 +389,20 @@ describe('middleware.karma', () => {
     callHandlerWith('/__karma__/context.html')
   })
 
+  it('should inline mappings without using special patterns', (done) => {
+    fsMock._touchFile('/karma/static/context.html', 0, '%MAPPINGS%')
+    servedFiles([
+      new MockFile('/.yarn/$$virtual/abc/a.js', 'sha_a')
+    ])
+
+    response.once('end', () => {
+      expect(response).to.beServedAs(200, "window.__karma__.files = {\n  '/__proxy__/__karma__/absolute/.yarn/$$virtual/abc/a.js': 'sha_a'\n};\n")
+      done()
+    })
+
+    callHandlerWith('/__karma__/context.html')
+  })
+
   it('should escape quotes in mappings with all served files', (done) => {
     fsMock._touchFile('/karma/static/context.html', 0, '%MAPPINGS%')
     servedFiles([
@@ -490,4 +520,19 @@ describe('middleware.karma', () => {
 
     callHandlerWith('/__karma__/debug.html')
   })
+
+  it('should serve client_with_context.html without using special patterns when replacing script urls', (done) => {
+    includedFiles([
+      new MockFile('/.yarn/$$virtual/first.js', 'sha123'),
+      new MockFile('/.yarn/$$virtual/second.dart', 'sha456')
+    ])
+
+    response.once('end', () => {
+      expect(nextSpy).not.to.have.been.called
+      expect(response).to.beServedAs(200, 'CLIENT_WITH_CONTEXT\nwindow.__karma__.scriptUrls = ["\\\\x3Cscript type=\\"text/javascript\\" src=\\"/__proxy__/__karma__/absolute/.yarn/$$virtual/first.js\\" crossorigin=\\"anonymous\\"\\\\x3E\\\\x3C/script\\\\x3E","\\\\x3Cscript type=\\"text/javascript\\" src=\\"/__proxy__/__karma__/absolute/.yarn/$$virtual/second.dart\\" crossorigin=\\"anonymous\\"\\\\x3E\\\\x3C/script\\\\x3E"];\n')
+      done()
+    })
+
+    callHandlerWith('/__karma__/client_with_context.html')
+  })
 })