@parcel/utils 2.0.0-nightly.92 → 2.0.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.
Files changed (93) hide show
  1. package/.eslintrc.js +6 -6
  2. package/lib/DefaultMap.js +26 -5
  3. package/lib/Deferred.js +10 -2
  4. package/lib/PromiseQueue.js +21 -30
  5. package/lib/TapStream.js +10 -10
  6. package/lib/alternatives.js +134 -0
  7. package/lib/ansi-html.js +10 -2
  8. package/lib/blob.js +14 -6
  9. package/lib/collection.js +0 -11
  10. package/lib/config.js +107 -34
  11. package/lib/countLines.js +2 -2
  12. package/lib/dependency-location.js +3 -3
  13. package/lib/generateBuildMetrics.js +148 -0
  14. package/lib/generateCertificate.js +33 -8
  15. package/lib/getExisting.js +11 -3
  16. package/lib/getRootDir.js +18 -7
  17. package/lib/glob.js +53 -19
  18. package/lib/hash.js +44 -0
  19. package/lib/http-server.js +48 -10
  20. package/lib/index.js +280 -222
  21. package/lib/is-url.js +12 -2
  22. package/lib/isDirectoryInside.js +24 -0
  23. package/lib/objectHash.js +10 -2
  24. package/lib/openInBrowser.js +94 -0
  25. package/lib/path.js +33 -6
  26. package/lib/prettyDiagnostic.js +107 -25
  27. package/lib/relativeBundlePath.js +13 -7
  28. package/lib/relativeUrl.js +19 -3
  29. package/lib/replaceBundleReferences.js +91 -35
  30. package/lib/schema.js +104 -33
  31. package/lib/sourcemap.js +147 -0
  32. package/lib/stream.js +38 -3
  33. package/lib/urlJoin.js +25 -6
  34. package/package.json +22 -16
  35. package/src/DefaultMap.js +25 -1
  36. package/src/PromiseQueue.js +16 -12
  37. package/src/alternatives.js +143 -0
  38. package/src/ansi-html.js +2 -2
  39. package/src/blob.js +3 -3
  40. package/src/bundle-url.js +1 -1
  41. package/src/collection.js +2 -14
  42. package/src/config.js +67 -34
  43. package/src/countLines.js +5 -2
  44. package/src/debounce.js +1 -1
  45. package/src/dependency-location.js +11 -6
  46. package/src/generateBuildMetrics.js +158 -0
  47. package/src/generateCertificate.js +1 -1
  48. package/src/getCertificate.js +1 -1
  49. package/src/getExisting.js +1 -4
  50. package/src/getRootDir.js +1 -2
  51. package/src/glob.js +29 -11
  52. package/src/hash.js +34 -0
  53. package/src/http-server.js +10 -12
  54. package/src/index.js +49 -23
  55. package/src/is-url.js +1 -1
  56. package/src/isDirectoryInside.js +11 -0
  57. package/src/openInBrowser.js +64 -0
  58. package/src/path.js +38 -6
  59. package/src/prettyDiagnostic.js +59 -28
  60. package/src/relativeBundlePath.js +8 -13
  61. package/src/replaceBundleReferences.js +75 -39
  62. package/src/schema.js +101 -44
  63. package/src/sourcemap.js +135 -0
  64. package/src/stream.js +31 -1
  65. package/src/urlJoin.js +3 -1
  66. package/test/DefaultMap.test.js +8 -5
  67. package/test/input/sourcemap/referenced-min.js +2 -0
  68. package/test/input/sourcemap/referenced-min.js.map +6 -0
  69. package/test/input/sourcemap/source-root.js +2 -0
  70. package/test/input/sourcemap/source-root.js.map +7 -0
  71. package/test/objectHash.test.js +33 -0
  72. package/test/prettifyTime.test.js +17 -0
  73. package/test/replaceBundleReferences.test.js +268 -0
  74. package/test/sourcemap.test.js +207 -0
  75. package/test/throttle.test.js +1 -2
  76. package/test/urlJoin.test.js +37 -0
  77. package/lib/generateBundleReport.js +0 -38
  78. package/lib/loadSourceMapUrl.js +0 -33
  79. package/lib/md5.js +0 -35
  80. package/lib/prettyError.js +0 -43
  81. package/lib/promisify.js +0 -13
  82. package/lib/resolve.js +0 -93
  83. package/lib/serializeObject.js +0 -28
  84. package/src/generateBundleReport.js +0 -51
  85. package/src/loadSourceMapUrl.js +0 -33
  86. package/src/md5.js +0 -44
  87. package/src/prettyError.js +0 -54
  88. package/src/promisify.js +0 -13
  89. package/src/resolve.js +0 -123
  90. package/src/serializeObject.js +0 -22
  91. package/test/input/sourcemap/referenced.js +0 -7
  92. package/test/loadSourceMapUrl.test.js +0 -37
  93. package/test/prettyError.test.js +0 -104
package/src/schema.js CHANGED
@@ -1,15 +1,20 @@
1
1
  // @flow strict-local
2
2
  import ThrowableDiagnostic, {
3
3
  generateJSONCodeHighlights,
4
+ escapeMarkdown,
5
+ encodeJSONKeyComponent,
4
6
  } from '@parcel/diagnostic';
5
- // $FlowFixMe untyped
6
- import levenshteinDistance from 'js-levenshtein';
7
+ import type {Mapping} from 'json-source-map';
8
+ import nullthrows from 'nullthrows';
9
+ // flowlint-next-line untyped-import:off
10
+ import levenshtein from 'fastest-levenshtein';
7
11
 
8
12
  export type SchemaEntity =
9
13
  | SchemaObject
10
14
  | SchemaArray
11
15
  | SchemaBoolean
12
16
  | SchemaString
17
+ | SchemaNumber
13
18
  | SchemaEnum
14
19
  | SchemaOneOf
15
20
  | SchemaAllOf
@@ -40,6 +45,11 @@ export type SchemaString = {|
40
45
  __validate?: (val: string) => ?string,
41
46
  __type?: string,
42
47
  |};
48
+ export type SchemaNumber = {|
49
+ type: 'number',
50
+ enum?: Array<number>,
51
+ __type?: string,
52
+ |};
43
53
  export type SchemaEnum = {|
44
54
  enum: Array<mixed>,
45
55
  |};
@@ -88,7 +98,7 @@ export type SchemaError =
88
98
  prop: string,
89
99
  expectedProps: Array<string>,
90
100
  actualProps: Array<string>,
91
- dataType: null | 'key',
101
+ dataType: 'key' | 'value',
92
102
 
93
103
  dataPath: string,
94
104
  ancestors: Array<SchemaEntity>,
@@ -99,7 +109,6 @@ export type SchemaError =
99
109
  actualValue: mixed,
100
110
  dataType: ?'key' | 'value',
101
111
  message?: string,
102
-
103
112
  dataPath: string,
104
113
  ancestors: Array<SchemaEntity>,
105
114
  |};
@@ -159,8 +168,7 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
159
168
  }
160
169
  } else if (schemaNode.__validate) {
161
170
  let validationError = schemaNode.__validate(value);
162
- // $FlowFixMe
163
- if (validationError) {
171
+ if (typeof validationError == 'string') {
164
172
  return {
165
173
  type: 'other',
166
174
  dataType: 'value',
@@ -173,6 +181,23 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
173
181
  }
174
182
  break;
175
183
  }
184
+ case 'number': {
185
+ // $FlowFixMe type was already checked
186
+ let value: number = dataNode;
187
+ if (schemaNode.enum) {
188
+ if (!schemaNode.enum.includes(value)) {
189
+ return {
190
+ type: 'enum',
191
+ dataType: 'value',
192
+ dataPath,
193
+ expectedValues: schemaNode.enum,
194
+ actualValue: value,
195
+ ancestors: schemaAncestors,
196
+ };
197
+ }
198
+ }
199
+ break;
200
+ }
176
201
  case 'object': {
177
202
  let results: Array<Array<SchemaError> | SchemaError> = [];
178
203
  let invalidProps;
@@ -187,7 +212,7 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
187
212
  k =>
188
213
  ({
189
214
  type: 'forbidden-prop',
190
- dataPath: dataPath + '/' + k,
215
+ dataPath: dataPath + '/' + encodeJSONKeyComponent(k),
191
216
  dataType: 'key',
192
217
  prop: k,
193
218
  expectedProps: Object.keys(schemaNode.properties),
@@ -209,7 +234,7 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
209
234
  ({
210
235
  type: 'missing-prop',
211
236
  dataPath,
212
- dataType: null,
237
+ dataType: 'value',
213
238
  prop: k,
214
239
  expectedProps: schemaNode.required,
215
240
  actualProps: keys,
@@ -230,7 +255,7 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
230
255
  [schemaNode.properties[k]].concat(schemaAncestors),
231
256
  // $FlowFixMe type was already checked
232
257
  dataNode[k],
233
- dataPath + '/' + k,
258
+ dataPath + '/' + encodeJSONKeyComponent(k),
234
259
  );
235
260
  if (result) results.push(result);
236
261
  } else {
@@ -239,7 +264,7 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
239
264
  results.push({
240
265
  type: 'enum',
241
266
  dataType: 'key',
242
- dataPath: dataPath + '/' + k,
267
+ dataPath: dataPath + '/' + encodeJSONKeyComponent(k),
243
268
  expectedValues: Object.keys(
244
269
  schemaNode.properties,
245
270
  ).filter(
@@ -256,7 +281,7 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
256
281
  [additionalProperties].concat(schemaAncestors),
257
282
  // $FlowFixMe type was already checked
258
283
  dataNode[k],
259
- dataPath + '/' + k,
284
+ dataPath + '/' + encodeJSONKeyComponent(k),
260
285
  );
261
286
  if (result) results.push(result);
262
287
  }
@@ -339,9 +364,12 @@ function validateSchema(schema: SchemaEntity, data: mixed): Array<SchemaError> {
339
364
  }
340
365
  export default validateSchema;
341
366
 
342
- function fuzzySearch(expectedValues: Array<string>, actualValue: string) {
367
+ export function fuzzySearch(
368
+ expectedValues: Array<string>,
369
+ actualValue: string,
370
+ ): Array<string> {
343
371
  let result = expectedValues
344
- .map(exp => [exp, levenshteinDistance(exp, actualValue)])
372
+ .map(exp => [exp, levenshtein.distance(exp, actualValue)])
345
373
  .filter(
346
374
  // Remove if more than half of the string would need to be changed
347
375
  ([, d]) => d * 2 < actualValue.length,
@@ -350,22 +378,43 @@ function fuzzySearch(expectedValues: Array<string>, actualValue: string) {
350
378
  return result.map(([v]) => v);
351
379
  }
352
380
 
353
- validateSchema.diagnostic = function(
381
+ validateSchema.diagnostic = function (
354
382
  schema: SchemaEntity,
355
- data: mixed,
356
- dataContentsPath?: ?string,
357
- dataContents: string | mixed,
383
+ data: {|
384
+ ...
385
+ | {|
386
+ source?: ?string,
387
+ data?: mixed,
388
+ |}
389
+ | {|
390
+ source: string,
391
+ map: {|
392
+ data: mixed,
393
+ pointers: {|[key: string]: Mapping|},
394
+ |},
395
+ |},
396
+ filePath?: ?string,
397
+ prependKey?: ?string,
398
+ |},
358
399
  origin: string,
359
- prependKey: string,
360
400
  message: string,
361
401
  ): void {
362
- let errors = validateSchema(schema, data);
402
+ if (
403
+ 'source' in data &&
404
+ 'data' in data &&
405
+ typeof data.source !== 'string' &&
406
+ !data
407
+ ) {
408
+ throw new Error(
409
+ 'At least one of data.source and data.source must be defined!',
410
+ );
411
+ }
412
+ let object = data.map
413
+ ? data.map.data
414
+ : // $FlowFixMe we can assume it's a JSON object
415
+ data.data ?? JSON.parse(data.source);
416
+ let errors = validateSchema(schema, object);
363
417
  if (errors.length) {
364
- let dataContentsString: string =
365
- typeof dataContents === 'string'
366
- ? dataContents
367
- : // $FlowFixMe
368
- JSON.stringify(dataContents, null, '\t');
369
418
  let keys = errors.map(e => {
370
419
  let message;
371
420
  if (e.type === 'enum') {
@@ -403,10 +452,8 @@ validateSchema.diagnostic = function(
403
452
  let {prop, actualProps} = e;
404
453
  let likely = fuzzySearch(actualProps, prop);
405
454
  if (likely.length > 0) {
406
- message = `Did you mean ${likely
407
- .map(v => JSON.stringify(v))
408
- .join(', ')}?`;
409
- e.dataPath += '/' + prop;
455
+ message = `Did you mean ${JSON.stringify(prop)}?`;
456
+ e.dataPath += '/' + likely[0];
410
457
  e.dataType = 'key';
411
458
  } else {
412
459
  message = `Missing property ${prop}`;
@@ -422,26 +469,36 @@ validateSchema.diagnostic = function(
422
469
  }
423
470
  return {key: e.dataPath, type: e.dataType, message};
424
471
  });
425
- let codeFrame = {
426
- code: dataContentsString,
427
- codeHighlights: generateJSONCodeHighlights(
428
- dataContentsString,
429
- keys.map(({key, type, message}) => ({
430
- key: prependKey + key,
431
- type: type,
432
- message,
433
- })),
434
- ),
435
- };
472
+ let map, code;
473
+ if (data.map) {
474
+ map = data.map;
475
+ code = data.source;
476
+ } else {
477
+ // $FlowFixMe we can assume that data is valid JSON
478
+ map = data.source ?? JSON.stringify(nullthrows(data.data), 0, '\t');
479
+ code = map;
480
+ }
481
+ let codeFrames = [
482
+ {
483
+ filePath: data.filePath ?? undefined,
484
+ language: 'json',
485
+ code,
486
+ codeHighlights: generateJSONCodeHighlights(
487
+ map,
488
+ keys.map(({key, type, message}) => ({
489
+ key: (data.prependKey ?? '') + key,
490
+ type: type,
491
+ message: message != null ? escapeMarkdown(message) : message,
492
+ })),
493
+ ),
494
+ },
495
+ ];
436
496
 
437
497
  throw new ThrowableDiagnostic({
438
498
  diagnostic: {
439
- message,
499
+ message: message,
440
500
  origin,
441
- // $FlowFixMe should be a sketchy string check
442
- filePath: dataContentsPath || undefined,
443
- language: 'json',
444
- codeFrame,
501
+ codeFrames,
445
502
  },
446
503
  });
447
504
  }
@@ -0,0 +1,135 @@
1
+ // @flow
2
+ import type {SourceLocation} from '@parcel/types';
3
+ import type {FileSystem} from '@parcel/fs';
4
+ import SourceMap from '@parcel/source-map';
5
+ import path from 'path';
6
+ import {normalizeSeparators, isAbsolute} from './path';
7
+
8
+ export const SOURCEMAP_RE: RegExp =
9
+ /(?:\/\*|\/\/)\s*[@#]\s*sourceMappingURL\s*=\s*([^\s*]+)(?:\s*\*\/)?\s*$/;
10
+ const DATA_URL_RE = /^data:[^;]+(?:;charset=[^;]+)?;base64,(.*)/;
11
+ export const SOURCEMAP_EXTENSIONS: Set<string> = new Set<string>([
12
+ 'css',
13
+ 'es',
14
+ 'es6',
15
+ 'js',
16
+ 'jsx',
17
+ 'mjs',
18
+ 'ts',
19
+ 'tsx',
20
+ ]);
21
+
22
+ export function matchSourceMappingURL(
23
+ contents: string,
24
+ ): null | RegExp$matchResult {
25
+ return contents.match(SOURCEMAP_RE);
26
+ }
27
+
28
+ export async function loadSourceMapUrl(
29
+ fs: FileSystem,
30
+ filename: string,
31
+ contents: string,
32
+ ): Promise<?{|filename: string, map: any, url: string|}> {
33
+ let match = matchSourceMappingURL(contents);
34
+ if (match) {
35
+ let url = match[1].trim();
36
+ let dataURLMatch = url.match(DATA_URL_RE);
37
+
38
+ let mapFilePath;
39
+ if (dataURLMatch) {
40
+ mapFilePath = filename;
41
+ } else {
42
+ mapFilePath = url.replace(/^file:\/\//, '');
43
+ mapFilePath = isAbsolute(mapFilePath)
44
+ ? mapFilePath
45
+ : path.join(path.dirname(filename), mapFilePath);
46
+ }
47
+
48
+ return {
49
+ url,
50
+ filename: mapFilePath,
51
+ map: JSON.parse(
52
+ dataURLMatch
53
+ ? Buffer.from(dataURLMatch[1], 'base64').toString()
54
+ : await fs.readFile(mapFilePath, 'utf8'),
55
+ ),
56
+ };
57
+ }
58
+ }
59
+
60
+ export async function loadSourceMap(
61
+ filename: string,
62
+ contents: string,
63
+ options: {fs: FileSystem, projectRoot: string, ...},
64
+ ): Promise<?SourceMap> {
65
+ let foundMap = await loadSourceMapUrl(options.fs, filename, contents);
66
+ if (foundMap) {
67
+ let mapSourceRoot = path.dirname(filename);
68
+ if (
69
+ foundMap.map.sourceRoot &&
70
+ !normalizeSeparators(foundMap.map.sourceRoot).startsWith('/')
71
+ ) {
72
+ mapSourceRoot = path.join(mapSourceRoot, foundMap.map.sourceRoot);
73
+ }
74
+
75
+ let sourcemapInstance = new SourceMap(options.projectRoot);
76
+ sourcemapInstance.addVLQMap({
77
+ ...foundMap.map,
78
+ sources: foundMap.map.sources.map(s => {
79
+ return path.join(mapSourceRoot, s);
80
+ }),
81
+ });
82
+ return sourcemapInstance;
83
+ }
84
+ }
85
+
86
+ export function remapSourceLocation(
87
+ loc: SourceLocation,
88
+ originalMap: SourceMap,
89
+ ): SourceLocation {
90
+ let {
91
+ filePath,
92
+ start: {line: startLine, column: startCol},
93
+ end: {line: endLine, column: endCol},
94
+ } = loc;
95
+ let lineDiff = endLine - startLine;
96
+ let colDiff = endCol - startCol;
97
+ let start = originalMap.findClosestMapping(startLine, startCol);
98
+ let end = originalMap.findClosestMapping(endLine, endCol);
99
+
100
+ if (start?.original) {
101
+ if (start.source) {
102
+ filePath = start.source;
103
+ }
104
+
105
+ ({line: startLine, column: startCol} = start.original);
106
+ startCol++; // source map columns are 0-based
107
+ }
108
+
109
+ if (end?.original) {
110
+ ({line: endLine, column: endCol} = end.original);
111
+ endCol++;
112
+
113
+ if (endLine < startLine) {
114
+ endLine = startLine;
115
+ endCol = startCol;
116
+ } else if (endLine === startLine && endCol < startCol && lineDiff === 0) {
117
+ endCol = startCol + colDiff;
118
+ }
119
+ } else {
120
+ endLine = startLine;
121
+ endCol = startCol;
122
+ }
123
+
124
+ return {
125
+ filePath,
126
+ start: {
127
+ line: startLine,
128
+ column: startCol,
129
+ },
130
+ end: {
131
+ line: endLine,
132
+ column: endCol,
133
+ },
134
+ };
135
+ }
package/src/stream.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @flow strict-local
2
2
 
3
- import {Readable} from 'stream';
3
+ import {Readable, PassThrough} from 'stream';
4
4
  import type {Blob} from '@parcel/types';
5
5
 
6
6
  export function measureStreamLength(stream: Readable): Promise<number> {
@@ -42,3 +42,33 @@ export function blobToStream(blob: Blob): Readable {
42
42
 
43
43
  return readableFromStringOrBuffer(blob);
44
44
  }
45
+
46
+ export function streamFromPromise(promise: Promise<Blob>): Readable {
47
+ const stream = new PassThrough();
48
+ promise.then(blob => {
49
+ if (blob instanceof Readable) {
50
+ blob.pipe(stream);
51
+ } else {
52
+ stream.end(blob);
53
+ }
54
+ });
55
+
56
+ return stream;
57
+ }
58
+
59
+ export function fallbackStream(
60
+ stream: Readable,
61
+ fallback: () => Readable,
62
+ ): Readable {
63
+ const res = new PassThrough();
64
+ stream.on('error', err => {
65
+ if (err.code === 'ENOENT') {
66
+ fallback().pipe(res);
67
+ } else {
68
+ res.emit('error', err);
69
+ }
70
+ });
71
+
72
+ stream.pipe(res);
73
+ return res;
74
+ }
package/src/urlJoin.js CHANGED
@@ -9,7 +9,9 @@ import path from 'path';
9
9
  */
10
10
  export default function urlJoin(publicURL: string, assetPath: string): string {
11
11
  const url = URL.parse(publicURL, false, true);
12
- const assetUrl = URL.parse(assetPath);
12
+ // Leading / ensures that paths with colons are not parsed as a protocol.
13
+ let p = assetPath.startsWith('/') ? assetPath : '/' + assetPath;
14
+ const assetUrl = URL.parse(p);
13
15
  url.pathname = path.posix.join(url.pathname, assetUrl.pathname);
14
16
  url.search = assetUrl.search;
15
17
  url.hash = assetUrl.hash;
@@ -1,14 +1,17 @@
1
1
  // @flow strict-local
2
2
 
3
3
  import assert from 'assert';
4
- import DefaultMap from '../src/DefaultMap';
4
+ import {DefaultMap} from '../src/DefaultMap';
5
5
 
6
6
  describe('DefaultMap', () => {
7
7
  it('constructs with entries just like Map', () => {
8
- let map = new DefaultMap(k => k, [
9
- [1, 3],
10
- [2, 27],
11
- ]);
8
+ let map = new DefaultMap(
9
+ k => k,
10
+ [
11
+ [1, 3],
12
+ [2, 27],
13
+ ],
14
+ );
12
15
  assert.equal(map.get(1), 3);
13
16
  assert.deepEqual(Array.from(map.entries()), [
14
17
  [1, 3],
@@ -0,0 +1,2 @@
1
+ function hello(){var l="Hello",o="world";console.log(l+" "+o+"!")}hello();
2
+ //# sourceMappingURL=file://referenced-min.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version":3,
3
+ "sources":["./referenced.js"],
4
+ "names":["hello","l","o","console","log"],
5
+ "mappings":"AAAA,SAASA,QACP,IAAIC,EAAI,QACNC,EAAI,QACNC,QAAQC,IAAIH,EAAI,IAAMC,EAAI,KAE5BF"
6
+ }
@@ -0,0 +1,2 @@
1
+ function hello(){var l="Hello",o="world";console.log(l+" "+o+"!")}hello();
2
+ //# sourceMappingURL=source-root.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version":3,
3
+ "sourceRoot": "../",
4
+ "sources":["./source.js"],
5
+ "names":["hello","l","o","console","log"],
6
+ "mappings":"AAAA,SAASA,QACP,IAAIC,EAAI,QACNC,EAAI,QACNC,QAAQC,IAAIH,EAAI,IAAMC,EAAI,KAE5BF"
7
+ }
@@ -0,0 +1,33 @@
1
+ // @flow
2
+ import assert from 'assert';
3
+ import objectHash from '../src/objectHash';
4
+
5
+ describe('objectHash', () => {
6
+ it('calculates the same hash for two different but deep equal objects', () => {
7
+ const obj1 = {
8
+ foo: {foo: 'foo', baz: ['foo', 'baz', 'bar'], bar: 'bar'},
9
+ baz: 'baz',
10
+ bar: 'bar',
11
+ };
12
+ const obj2 = {
13
+ foo: {foo: 'foo', baz: ['foo', 'baz', 'bar'], bar: 'bar'},
14
+ baz: 'baz',
15
+ bar: 'bar',
16
+ };
17
+
18
+ assert.equal(objectHash(obj1), objectHash(obj2));
19
+ });
20
+
21
+ it('calculates a unique hash for two deep equal objects', () => {
22
+ const obj1 = {
23
+ baz: 'baz',
24
+ bar: 'ba',
25
+ };
26
+ const obj2 = {
27
+ baz: 'baz',
28
+ bar: 'bar',
29
+ };
30
+
31
+ assert.notEqual(objectHash(obj1), objectHash(obj2));
32
+ });
33
+ });
@@ -0,0 +1,17 @@
1
+ // @flow
2
+ import assert from 'assert';
3
+ import prettifyTime from '../src/prettifyTime';
4
+
5
+ describe('prettifyTime', () => {
6
+ it('should format numbers less than 1000 as ms', () => {
7
+ assert.equal(prettifyTime(888), '888ms');
8
+ assert.equal(prettifyTime(50), '50ms');
9
+ assert.equal(prettifyTime(0), '0ms');
10
+ });
11
+
12
+ it('should format numbers greater than 1000 as s with 2 fractional digits', () => {
13
+ assert.equal(prettifyTime(4000), '4.00s');
14
+ assert.equal(prettifyTime(90000), '90.00s');
15
+ assert.equal(prettifyTime(45678), '45.68s');
16
+ });
17
+ });