jasmine-browser-runner 2.0.0-beta.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,9 @@ Its value can be `"firefox"`, `"headlessFirefox"`, `"safari"`,
68
68
  ## ES module support
69
69
 
70
70
  If a source, spec, or helper file's name ends in `.mjs`, it will be loaded as
71
- an ES module rather than a regular script.
71
+ an ES module rather than a regular script. Note that ES modules can only be
72
+ loaded from other ES modules. So if your source files are ES modules, your
73
+ spec files need to be ES modules too.
72
74
 
73
75
  To allow spec files to import source files via relative paths, set the `specDir`
74
76
  config field to something that's high enough up to include both spec and source
@@ -78,6 +80,23 @@ running `npx jasmine-browser-runner init --esm`.
78
80
  If you have specs or helper files that use top-level await, set the
79
81
  `enableTopLevelAwait` config property is set to `true`.
80
82
 
83
+ [Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
84
+ are also supported:
85
+
86
+ ```javascript
87
+ {
88
+ // ...
89
+ "importMap": {
90
+ "moduleRootDir": "node_modules",
91
+ "imports": {
92
+ "some-lib":"some-lib/dist/index.mjs",
93
+ "some-lib/": "some-lib/dist/",
94
+ "some-cdn-lib": "https://example.com/some-cdn-lib"
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
81
100
  ## Use with Rails
82
101
 
83
102
  You can use jasmine-browser-runner to test your Rails application's JavaScript,
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- var path = require('path'),
3
+ const path = require('path'),
4
4
  jasmineCore = require('../lib/jasmineCore'),
5
5
  Command = require('../lib/command'),
6
6
  jasmineBrowser = require('../index.js');
7
7
  const UsageError = require('../lib/usage_error');
8
8
 
9
- var command = new Command({
9
+ const command = new Command({
10
10
  baseDir: path.resolve(),
11
11
  jasmineCore,
12
12
  jasmineBrowser,
package/lib/command.js CHANGED
@@ -250,7 +250,7 @@ function wrapDescription(indentLevel, prefixWidth, maxWidth, text) {
250
250
  return text;
251
251
  }
252
252
 
253
- var chunks = [];
253
+ const chunks = [];
254
254
  while (text.length > columnWidth) {
255
255
  const wrapChar = text.lastIndexOf(' ', columnWidth);
256
256
  chunks.push(text.substring(0, wrapChar));
package/lib/config.js CHANGED
@@ -50,6 +50,10 @@ function validateConfig(config) {
50
50
  throw new Error(`Configuration's ${k} property is not an array`);
51
51
  }
52
52
  }
53
+
54
+ if (config.importMap) {
55
+ validateImportMap(config.importMap);
56
+ }
53
57
  }
54
58
 
55
59
  function defaultConfig() {
@@ -67,6 +71,83 @@ function defaultEsmConfig() {
67
71
  );
68
72
  }
69
73
 
74
+ function validateImportMap(importMap) {
75
+ // basic validation, but not the entire spec.
76
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
77
+
78
+ if (typeof importMap !== 'object') {
79
+ throw new Error("Configuration's importMap property is not an object");
80
+ }
81
+
82
+ if (!importMap.imports && !importMap.scopes) {
83
+ throw new Error("Configuration's importMap contains no imports or scopes");
84
+ }
85
+
86
+ if (importMap.imports) {
87
+ validateImports('importMap.imports', importMap.imports);
88
+ }
89
+
90
+ if (importMap.scopes) {
91
+ for (const [kScope, vScope] of Object.entries(importMap.scopes)) {
92
+ if (kScope === '') {
93
+ throw new Error(
94
+ "Configuration's importMap.scopes cannot contain empty keys"
95
+ );
96
+ }
97
+ if (typeof vScope !== 'object') {
98
+ throw new Error(
99
+ "Configuration's importMap.scopes property is not an object"
100
+ );
101
+ }
102
+ validateImports('importMap.scopes', vScope);
103
+ }
104
+ }
105
+
106
+ const moduleRootDir = importMap.moduleRootDir;
107
+
108
+ if (moduleRootDir === '') {
109
+ throw new Error(
110
+ 'Configuration.importMap.moduleRootDir cannot be an empty string'
111
+ );
112
+ } else if (moduleRootDir && moduleRootDir.startsWith('../')) {
113
+ throw new Error(
114
+ 'Configuration.importMap.moduleRootDir cannot start with ../'
115
+ );
116
+ }
117
+ }
118
+
119
+ // Both importMap.imports and importMap.scopes[k].value should be an object with
120
+ // module specifiers, hence this helper function for DRY.
121
+ // @param context `importMap.imports` | `importMap.scopes`
122
+ // @param module specifier map of string -> string
123
+ function validateImports(context, imports) {
124
+ if (typeof imports !== 'object') {
125
+ throw new Error(`Configuration's ${context} property is not an object`);
126
+ }
127
+
128
+ if (Object.keys(imports).length === 0) {
129
+ throw new Error(`Configuration's ${context} map cannot be empty`);
130
+ }
131
+
132
+ for (const [kImport, vImport] of Object.entries(imports)) {
133
+ if (kImport === '') {
134
+ throw new Error(
135
+ `Configuration's ${context} map cannot contain empty string keys`
136
+ );
137
+ }
138
+
139
+ if (vImport === '') {
140
+ throw new Error(
141
+ `Configuration's ${context} map cannot contain empty string values`
142
+ );
143
+ }
144
+
145
+ if (typeof vImport !== 'string') {
146
+ throw new Error(`Configuration's ${context} map value is not a string`);
147
+ }
148
+ }
149
+ }
150
+
70
151
  module.exports = {
71
152
  loadConfig,
72
153
  validateConfig,
package/lib/server.js CHANGED
@@ -40,7 +40,7 @@ class Server {
40
40
  }
41
41
 
42
42
  allCss() {
43
- var urls = this.getUrls(
43
+ const urls = this.getUrls(
44
44
  this.options.srcDir,
45
45
  this.options.cssFiles,
46
46
  '/__src__'
@@ -71,7 +71,7 @@ class Server {
71
71
  }
72
72
 
73
73
  userJs() {
74
- var srcUrls = this.getUrls(
74
+ const srcUrls = this.getUrls(
75
75
  this.options.srcDir,
76
76
  this.options.srcFiles,
77
77
  '/__src__'
@@ -79,12 +79,12 @@ class Server {
79
79
  // Exclude ES modules. These will be loaded by other ES modules.
80
80
  return !url.endsWith('.mjs');
81
81
  });
82
- var helperUrls = this.getUrls(
82
+ const helperUrls = this.getUrls(
83
83
  this.options.specDir,
84
84
  this.options.helpers,
85
85
  '/__spec__'
86
86
  );
87
- var specUrls = this.getUrls(
87
+ const specUrls = this.getUrls(
88
88
  this.options.specDir,
89
89
  this.options.specFiles,
90
90
  '/__spec__'
@@ -92,6 +92,33 @@ class Server {
92
92
  return [].concat(srcUrls, helperUrls, specUrls);
93
93
  }
94
94
 
95
+ // Bound property for ejs template, used to inject a `<script
96
+ // type="importmap">` tag. This changes specifier target values for any
97
+ // relative module paths according to `this.options.importMap.moduleRootDir`.
98
+ // @returns ImportMap | undefined
99
+ importMap() {
100
+ const { importMap } = this.options;
101
+ if (!importMap) {
102
+ return undefined;
103
+ }
104
+
105
+ const resultMap = {};
106
+
107
+ if (importMap.imports) {
108
+ resultMap.imports = reifyRawSpecifierMap(importMap.imports);
109
+ }
110
+
111
+ if (importMap.scopes) {
112
+ resultMap.scopes = {};
113
+
114
+ for (const [scope, map] of Object.entries(importMap.scopes)) {
115
+ resultMap.scopes[scope] = reifyRawSpecifierMap(map);
116
+ }
117
+ }
118
+
119
+ return resultMap;
120
+ }
121
+
95
122
  /**
96
123
  * Starts the server.
97
124
  * @param {ServerStartOptions} options
@@ -99,7 +126,7 @@ class Server {
99
126
  */
100
127
  start(serverOptions) {
101
128
  serverOptions = serverOptions || {};
102
- var app = express();
129
+ const app = express();
103
130
 
104
131
  app.use('/__jasmine__', express.static(this.jasmineCore.files.path));
105
132
  app.use('/__boot__', express.static(this.jasmineCore.files.bootDir));
@@ -114,14 +141,21 @@ class Server {
114
141
  express.static(path.join(this.projectBaseDir, this.options.srcDir))
115
142
  );
116
143
 
117
- var indexTemplate = ejs.compile(
144
+ if (this.options.importMap) {
145
+ const dir = this.options.importMap.moduleRootDir
146
+ ? path.join(this.projectBaseDir, this.options.importMap.moduleRootDir)
147
+ : this.projectBaseDir;
148
+ app.use('/__moduleRoot__', express.static(dir));
149
+ }
150
+
151
+ const indexTemplate = ejs.compile(
118
152
  fs.readFileSync(path.resolve(__dirname, '../run.html.ejs')).toString()
119
153
  );
120
- var configTemplate = ejs.compile(
154
+ const configTemplate = ejs.compile(
121
155
  fs.readFileSync(path.resolve(__dirname, '../config.js.ejs')).toString()
122
156
  );
123
157
 
124
- var self = this;
158
+ const self = this;
125
159
  app.get('/', function(req, res) {
126
160
  try {
127
161
  res.send(
@@ -129,6 +163,7 @@ class Server {
129
163
  cssFiles: self.allCss(),
130
164
  jasmineJsFiles: self.jasmineJs(),
131
165
  userJsFiles: self.userJs(),
166
+ importMap: self.importMap(),
132
167
  enableTopLevelAwait: self.options.enableTopLevelAwait || false,
133
168
  })
134
169
  );
@@ -151,7 +186,7 @@ class Server {
151
186
  }
152
187
  });
153
188
 
154
- var port = findPort(serverOptions.port, this.options.port);
189
+ const port = findPort(serverOptions.port, this.options.port);
155
190
  return new Promise(resolve => {
156
191
  this._httpServer = app.listen(port, () => {
157
192
  const runningPort = this._httpServer.address().port;
@@ -258,4 +293,22 @@ function unWindows(filePath) {
258
293
  return filePath.replace(/\\/g, '/');
259
294
  }
260
295
 
296
+ // Processes the incoming `rawSpecifierMap`, converting targets to use actual
297
+ // paths that the run.html.ejs file will contain. The `rawSpecifierMap` is not
298
+ // the entire importMap. It is a key/value map that may be the "imports" value
299
+ // or an individual map inside of "scopes"[someScope].
300
+ function reifyRawSpecifierMap(rawSpecifierMap) {
301
+ const concreteMap = {};
302
+
303
+ for (const [key, value] of Object.entries(rawSpecifierMap)) {
304
+ if (value.match(/^https?:\/\//)) {
305
+ concreteMap[key] = value; // pass through unchanged
306
+ } else {
307
+ concreteMap[key] = './' + unWindows(path.join('__moduleRoot__', value));
308
+ }
309
+ }
310
+
311
+ return concreteMap;
312
+ }
313
+
261
314
  module.exports = Server;
@@ -1,9 +1,9 @@
1
1
  /* eslint-env browser, jasmine */
2
2
  function BatchReporter() {
3
- var events = [];
3
+ let events = [];
4
4
 
5
5
  this.getBatch = function() {
6
- var result = events;
6
+ const result = events;
7
7
  events = [];
8
8
  return result;
9
9
  };
@@ -1,14 +1,14 @@
1
1
  /* eslint-env browser, jasmine */
2
2
 
3
3
  window._jasmine_loadEsModule = function(src) {
4
- var script = document.createElement('script');
4
+ const script = document.createElement('script');
5
5
  script.type = 'module';
6
6
 
7
7
  // Safari reports syntax errors in ES modules as a script element error
8
8
  // event rather than a global error event. Rethrow so that Jasmine can
9
9
  // pick it up and fail the suite.
10
10
  script.addEventListener('error', function(event) {
11
- var msg =
11
+ const msg =
12
12
  'An error occurred while loading ' +
13
13
  src +
14
14
  '. Check the browser console for details.';
package/lib/types.js CHANGED
@@ -111,6 +111,14 @@
111
111
  * @type boolean | undefined
112
112
  * @default true
113
113
  */
114
+ /**
115
+ * Import maps entry to generate the `<script type="importmap">` section in the
116
+ * `<head>`, to enable ES Module testing in the browser.
117
+ *
118
+ * @name Configuration#importMap
119
+ * @type ImportMap | undefined
120
+ * @default undefined
121
+ */
114
122
  /**
115
123
  * Whether to enable support for top-level await. This option is off by default
116
124
  * because it comes with a performance penalty.
@@ -197,3 +205,37 @@
197
205
  * @name ServerStartOptions#port
198
206
  * @type number | undefined
199
207
  */
208
+
209
+ /**
210
+ * Describes an import map.
211
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap}
212
+ * @see {@link https://github.com/WICG/import-maps}
213
+ * @interface ImportMap
214
+ */
215
+ /**
216
+ * A single, unscoped module specifier map.
217
+ * @name ImportMap#imports
218
+ * @type {Object.<string, string>}
219
+ */
220
+ /**
221
+ * Map of one or more scoped module specifier maps.
222
+ * @name ImportMap#scopes
223
+ * @type {Object.<string, Object.<string, string>>}
224
+ */
225
+ /**
226
+ * Optional directory that specifies the root for relative paths in import map
227
+ * (if required).
228
+ *
229
+ * For example, if you only use import paths that resolve to absolute targets,
230
+ * e.g. 'my-pkg': 'https://mycdn.url/my-pkg', then you do not need this dir
231
+ * option. But if you need to reference a folder, e.g. `node_modules`, then this
232
+ * is required.
233
+ *
234
+ * moduleRootDir is evaluated relative to the project base directory, which is
235
+ * typically the current working directory from which jasmine-browser-runner is
236
+ * run.
237
+ *
238
+ * @name ImportMap#moduleRootDir
239
+ * @type string
240
+ * @example 'node_modules'
241
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jasmine-browser-runner",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.1",
4
4
  "description": "Serve and run your Jasmine specs in a browser",
5
5
  "bin": "bin/jasmine-browser-runner",
6
6
  "exports": "./index.js",
@@ -115,7 +115,8 @@
115
115
  "always"
116
116
  ],
117
117
  "space-before-blocks": "error",
118
- "no-console": "off"
118
+ "no-console": "off",
119
+ "no-var": "error"
119
120
  },
120
121
  "overrides": [
121
122
  {
package/run.html.ejs CHANGED
@@ -8,6 +8,11 @@
8
8
  <% cssFiles.forEach(function(cssFile) { %>
9
9
  <link rel="stylesheet" href="<%= cssFile %>" type="text/css" media="screen"/>
10
10
  <% }) %>
11
+ <% if (importMap) { %>
12
+ <script type="importmap">
13
+ <%- JSON.stringify(importMap, null, 2) %>
14
+ </script>
15
+ <% } %>
11
16
  </head>
12
17
  <body>
13
18
  <% jasmineJsFiles.forEach(function(jsFile) { %>