reslop 0.1.0 → 0.1.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/lib/github.js ADDED
@@ -0,0 +1,631 @@
1
+ 'use strict';
2
+
3
+ const { delay } = require('metautil');
4
+
5
+ const diff = require('./diff.js');
6
+ const { parseDiff, itemsFromFiles } = diff;
7
+ const { itemPath } = require('./files.js');
8
+
9
+ const GITHUB_API = 'https://api.github.com';
10
+ const API_VERSION = '2022-11-28';
11
+ const USER_AGENT = 'reslop';
12
+ const JSON_ACCEPT = 'application/vnd.github+json';
13
+ const DIFF_ACCEPT = 'application/vnd.github.diff';
14
+ const RETRY_ATTEMPTS = 3;
15
+ const RETRY_DELAY_MS = 200;
16
+ const TRANSIENT_STATUS = [429, 502, 503, 504];
17
+ const PR_TODO_FILE = 'pull request';
18
+ const LIST_PER_PAGE = 100;
19
+ const GRAPHQL_URL = `${GITHUB_API}/graphql`;
20
+ const REVIEW_THREADS_QUERY =
21
+ 'query($owner: String!, $name: String!, $number: Int!, $after: String) {' +
22
+ ' repository(owner: $owner, name: $name) {' +
23
+ ' pullRequest(number: $number) {' +
24
+ ' reviewThreads(first: 100, after: $after) {' +
25
+ ' pageInfo { hasNextPage endCursor }' +
26
+ ' nodes { isResolved comments(first: 100) { nodes { databaseId } } }' +
27
+ ' } } } }';
28
+
29
+ const GITHUB_PR_RE = new RegExp(
30
+ String.raw`^(?:https?://)?(?:www\.)?github\.com/` +
31
+ String.raw`([^/?#]+)/([^/?#]+)/pull/(\d+)` +
32
+ String.raw`(?:\.diff|\.patch)?` +
33
+ String.raw`(?:/(?:files|commits|checks|changes)?)?` +
34
+ String.raw`/?(?:[?#].*)?$`,
35
+ 'i',
36
+ );
37
+
38
+ const isRecord = (value) =>
39
+ !!value && typeof value === 'object' && !Array.isArray(value);
40
+
41
+ const oneLine = (text) => {
42
+ const value = `${text ?? ''}`.trim();
43
+ if (!value) return '';
44
+ return value.split('\n')[0];
45
+ };
46
+
47
+ const githubError = (code, message) => {
48
+ const error = new Error(message);
49
+ error.code = code;
50
+ return error;
51
+ };
52
+
53
+ const withStatus = (error, status) => {
54
+ error.status = status;
55
+ return error;
56
+ };
57
+
58
+ const parseGithubPrUrl = (text) => {
59
+ if (typeof text !== 'string' || !text) return null;
60
+ const match = GITHUB_PR_RE.exec(text.trim());
61
+ if (!match) return null;
62
+ const owner = match[1];
63
+ const rawRepo = match[2];
64
+ const repo = rawRepo.replace(/\.git$/i, '');
65
+ const number = parseInt(match[3], 10);
66
+ const hasNumber = Number.isFinite(number) && number >= 1;
67
+ if (!owner || !repo || !hasNumber) return null;
68
+ return { owner, repo, number };
69
+ };
70
+
71
+ const githubToken = (env) => {
72
+ const from = env ?? process.env;
73
+ if (!isRecord(from)) return '';
74
+ return from.GITHUB_TOKEN || from.GH_TOKEN || '';
75
+ };
76
+
77
+ const parseApiMessage = (body) => {
78
+ const text = `${body ?? ''}`.trim();
79
+ if (!text) return '';
80
+ try {
81
+ const parsed = JSON.parse(text);
82
+ if (isRecord(parsed) && typeof parsed.message === 'string') {
83
+ return parsed.message;
84
+ }
85
+ } catch {
86
+ // ignore: body is not JSON
87
+ }
88
+ return oneLine(text);
89
+ };
90
+
91
+ const githubHttpError = (status, body, token) => {
92
+ const message = parseApiMessage(body);
93
+ if (status === 404) {
94
+ const hint = token ? '' : ' (set GITHUB_TOKEN for private repositories)';
95
+ const error = githubError(
96
+ 'GITHUB_NOT_FOUND',
97
+ `GitHub pull request not found${hint}`,
98
+ );
99
+ return withStatus(error, status);
100
+ }
101
+ if (status === 401) {
102
+ const error = githubError('GITHUB_AUTH', 'GitHub authentication failed');
103
+ return withStatus(error, status);
104
+ }
105
+ if (status === 403 && /rate limit/i.test(message)) {
106
+ const error = githubError(
107
+ 'GITHUB_RATE_LIMIT',
108
+ 'GitHub API rate limit exceeded',
109
+ );
110
+ return withStatus(error, status);
111
+ }
112
+ const detail = message
113
+ ? `GitHub API: ${oneLine(message)}`
114
+ : `GitHub API HTTP ${status}`;
115
+ return withStatus(githubError('GITHUB_HTTP', detail), status);
116
+ };
117
+
118
+ const wrapNetworkError = (error) => {
119
+ const detail = oneLine(error && error.message);
120
+ const suffix = detail ? `: ${detail}` : '';
121
+ const wrapped = githubError(
122
+ 'GITHUB_NETWORK',
123
+ `GitHub request failed${suffix}`,
124
+ );
125
+ wrapped.cause = error;
126
+ return wrapped;
127
+ };
128
+
129
+ const isRetryable = (error) => {
130
+ if (error.code === 'GITHUB_NETWORK') return true;
131
+ return TRANSIENT_STATUS.includes(error.status);
132
+ };
133
+
134
+ const retry = async (run, options) => {
135
+ const attempts = options.attempts ?? RETRY_ATTEMPTS;
136
+ const delayMs = options.delayMs ?? RETRY_DELAY_MS;
137
+ let lastError = null;
138
+ for (let i = 0; i < attempts; i++) {
139
+ try {
140
+ return await run();
141
+ } catch (error) {
142
+ lastError = error;
143
+ const isLast = i === attempts - 1;
144
+ if (isLast || !isRetryable(error)) throw error;
145
+ await delay(delayMs * (i + 1));
146
+ }
147
+ }
148
+ throw lastError;
149
+ };
150
+
151
+ const githubHeaders = (token, accept) => {
152
+ const headers = {
153
+ Accept: accept,
154
+ 'User-Agent': USER_AGENT,
155
+ 'X-GitHub-Api-Version': API_VERSION,
156
+ };
157
+ if (!token) return headers;
158
+ return { ...headers, Authorization: `Bearer ${token}` };
159
+ };
160
+
161
+ const headerValue = (headers, name) => {
162
+ if (!headers) return '';
163
+ if (typeof headers.get === 'function') {
164
+ return headers.get(name) || '';
165
+ }
166
+ const wanted = name.toLowerCase();
167
+ for (const [key, value] of Object.entries(headers)) {
168
+ if (key.toLowerCase() === wanted) return `${value}`;
169
+ }
170
+ return '';
171
+ };
172
+
173
+ const parseLinkNext = (headers) => {
174
+ const link = headerValue(headers, 'link');
175
+ if (!link) return '';
176
+ for (const part of link.split(',')) {
177
+ const match = /<([^>]+)>\s*;\s*rel="?next"?/i.exec(part.trim());
178
+ if (match) return match[1];
179
+ }
180
+ return '';
181
+ };
182
+
183
+ const parseGithubJson = (text, label) => {
184
+ let data;
185
+ try {
186
+ data = JSON.parse(text);
187
+ } catch {
188
+ throw githubError('GITHUB_PARSE', `GitHub API: invalid ${label}`);
189
+ }
190
+ return data;
191
+ };
192
+
193
+ const requestGithub = async (url, options) => {
194
+ const fetchImpl = options.fetch;
195
+ if (typeof fetchImpl !== 'function') {
196
+ throw githubError('GITHUB_FETCH', 'fetch is not available');
197
+ }
198
+ const token = options.token || '';
199
+ const headers = { ...githubHeaders(token, options.accept) };
200
+ if (options.contentType) headers['Content-Type'] = options.contentType;
201
+ const retryOpts = options.retry ?? {};
202
+ const method = options.method || 'GET';
203
+ const body = options.body;
204
+ return retry(async () => {
205
+ let response;
206
+ try {
207
+ const init = { method, headers };
208
+ if (body !== undefined) init.body = body;
209
+ response = await fetchImpl(url, init);
210
+ } catch (error) {
211
+ throw wrapNetworkError(error);
212
+ }
213
+ const text = await response.text();
214
+ if (!response.ok) throw githubHttpError(response.status, text, token);
215
+ return { body: text, headers: response.headers };
216
+ }, retryOpts);
217
+ };
218
+
219
+ const requestGithubList = async (url, options) => {
220
+ const items = [];
221
+ let next = url;
222
+ while (next) {
223
+ const page = await requestGithub(next, {
224
+ ...options,
225
+ accept: JSON_ACCEPT,
226
+ });
227
+ const data = parseGithubJson(page.body, 'list payload');
228
+ if (!Array.isArray(data)) {
229
+ throw githubError('GITHUB_PARSE', 'GitHub API: invalid list payload');
230
+ }
231
+ for (const entry of data) items.push(entry);
232
+ next = parseLinkNext(page.headers);
233
+ }
234
+ return items;
235
+ };
236
+
237
+ const requestGraphql = async (query, variables, options) => {
238
+ const payload = JSON.stringify({ query, variables });
239
+ const page = await requestGithub(GRAPHQL_URL, {
240
+ ...options,
241
+ method: 'POST',
242
+ accept: JSON_ACCEPT,
243
+ contentType: 'application/json',
244
+ body: payload,
245
+ });
246
+ const data = parseGithubJson(page.body, 'GraphQL payload');
247
+ if (!isRecord(data)) {
248
+ throw githubError('GITHUB_PARSE', 'GitHub API: invalid GraphQL payload');
249
+ }
250
+ return data;
251
+ };
252
+
253
+ const changePath = (file) => file.newPath || file.oldPath || '';
254
+
255
+ const normalizePathSpec = (spec) =>
256
+ `${spec ?? ''}`.replaceAll('\\', '/').replace(/\/+$/, '');
257
+
258
+ const isPathInScope = (rel, paths) => {
259
+ if (!paths.length) return true;
260
+ for (const spec of paths) {
261
+ const norm = normalizePathSpec(spec);
262
+ if (!norm || norm === '.') return true;
263
+ if (rel === norm || rel.startsWith(`${norm}/`)) return true;
264
+ }
265
+ return false;
266
+ };
267
+
268
+ const filterChangeFiles = (files, paths) => {
269
+ if (!paths.length) return files;
270
+ const kept = [];
271
+ for (const file of files) {
272
+ if (isPathInScope(changePath(file), paths)) kept.push(file);
273
+ }
274
+ return kept;
275
+ };
276
+
277
+ const prApiUrl = (pr) => {
278
+ const owner = encodeURIComponent(pr.owner);
279
+ const repo = encodeURIComponent(pr.repo);
280
+ return `${GITHUB_API}/repos/${owner}/${repo}/pulls/${pr.number}`;
281
+ };
282
+
283
+ const listUrl = (base) => {
284
+ const url = new URL(base);
285
+ url.searchParams.set('per_page', `${LIST_PER_PAGE}`);
286
+ return url.toString();
287
+ };
288
+
289
+ const discussionUrls = (pr) => {
290
+ const pull = prApiUrl(pr);
291
+ const issue = pull.replace('/pulls/', '/issues/');
292
+ return {
293
+ reviewComments: listUrl(`${pull}/comments`),
294
+ reviews: listUrl(`${pull}/reviews`),
295
+ issueComments: listUrl(`${issue}/comments`),
296
+ };
297
+ };
298
+
299
+ const readLogin = (user) => {
300
+ if (!isRecord(user)) return '';
301
+ return user.login || '';
302
+ };
303
+
304
+ const readRef = (side) => {
305
+ if (!isRecord(side)) return '';
306
+ return side.ref || '';
307
+ };
308
+
309
+ const readRepoName = (data, pr) => {
310
+ const base = isRecord(data.base) ? data.base : null;
311
+ const repo = base && isRecord(base.repo) ? base.repo : null;
312
+ const fromBase = repo && repo['full_name'];
313
+ if (fromBase) return fromBase;
314
+ return `${pr.owner}/${pr.repo}`;
315
+ };
316
+
317
+ const changeFiles = (items) => {
318
+ const files = [];
319
+ const seen = new Set();
320
+ for (const item of items) {
321
+ const rel = itemPath(item);
322
+ if (!rel || seen.has(rel)) continue;
323
+ seen.add(rel);
324
+ files.push(rel);
325
+ }
326
+ return files;
327
+ };
328
+
329
+ const parsePullRequestPayload = (text) => {
330
+ const data = parseGithubJson(text, 'pull request payload');
331
+ if (!isRecord(data)) {
332
+ throw githubError(
333
+ 'GITHUB_PARSE',
334
+ 'GitHub API: invalid pull request payload',
335
+ );
336
+ }
337
+ return data;
338
+ };
339
+
340
+ const toChange = (pr, data, items) => {
341
+ const number = data.number || pr.number;
342
+ const repository = readRepoName(data, pr);
343
+ const title = data.title || '';
344
+ const author = readLogin(data.user);
345
+ const files = changeFiles(items);
346
+ const base = readRef(data.base);
347
+ const head = readRef(data.head);
348
+ const fallbackUrl = `https://github.com/${repository}/pull/${number}`;
349
+ const url = data['html_url'] || fallbackUrl;
350
+ const id = `github.com/${repository}/pull/${number}`;
351
+ return {
352
+ id,
353
+ source: 'github-pr',
354
+ title,
355
+ author,
356
+ files,
357
+ status: 'reviewing',
358
+ base,
359
+ head,
360
+ url,
361
+ repository,
362
+ number,
363
+ };
364
+ };
365
+
366
+ const readCommentBody = (value) => `${value ?? ''}`.trim();
367
+
368
+ const reviewerOf = (record) => {
369
+ const login = readLogin(record && record.user);
370
+ return login || 'unknown';
371
+ };
372
+
373
+ const formatImportedText = (entry) => {
374
+ const reviewer = entry.reviewer || 'unknown';
375
+ const body = `${entry.body ?? ''}`.trim();
376
+ return `@${reviewer} review at github: ${body}`;
377
+ };
378
+
379
+ const commentLine = (comment) => {
380
+ const side = comment.side === 'LEFT' ? 'LEFT' : 'RIGHT';
381
+ const raw = comment.line ?? comment['original_line'];
382
+ const line = Number(raw);
383
+ if (!Number.isFinite(line) || line < 1) return { side, line: 0 };
384
+ return { side, line };
385
+ };
386
+
387
+ const sideLineNumber = (type, oldNo, newNo, side) => {
388
+ if (side === 'LEFT') {
389
+ if (type === 'add') return 0;
390
+ return oldNo;
391
+ }
392
+ if (type === 'del') return 0;
393
+ return newNo;
394
+ };
395
+
396
+ const locateLineInItem = (item, rel, line, side) => {
397
+ const path = itemPath(item);
398
+ if (path !== rel) return null;
399
+ const hunk = item.hunk;
400
+ if (!hunk || !line) return null;
401
+ let oldNo = hunk.oldStart;
402
+ let newNo = hunk.newStart;
403
+ for (const entry of hunk.lines) {
404
+ const at = sideLineNumber(entry.type, oldNo, newNo, side);
405
+ if (at === line) {
406
+ const inBlock = entry.blockId === item.blockId;
407
+ return { inBlock };
408
+ }
409
+ if (entry.type === 'ctx' || entry.type === 'del') oldNo += 1;
410
+ if (entry.type === 'ctx' || entry.type === 'add') newNo += 1;
411
+ }
412
+ return null;
413
+ };
414
+
415
+ const findItemForLine = (items, rel, line, side) => {
416
+ let fallback = null;
417
+ for (const item of items) {
418
+ const hit = locateLineInItem(item, rel, line, side);
419
+ if (!hit) continue;
420
+ if (hit.inBlock) return item;
421
+ if (!fallback) fallback = item;
422
+ }
423
+ return fallback;
424
+ };
425
+
426
+ const resolvedOf = (resolvedById, id) => {
427
+ const commentId = Number(id);
428
+ if (!Number.isFinite(commentId) || !resolvedById.has(commentId)) {
429
+ return null;
430
+ }
431
+ return resolvedById.get(commentId);
432
+ };
433
+
434
+ const toImportedTodo = (file, entry) => ({
435
+ kind: 'todo',
436
+ file,
437
+ text: formatImportedText(entry),
438
+ done: entry.resolved === true,
439
+ });
440
+
441
+ const reviewCommentToNote = (comment, items, resolvedById, paths) => {
442
+ if (!isRecord(comment)) return null;
443
+ const body = readCommentBody(comment.body);
444
+ if (!body) return null;
445
+ const path = comment.path || '';
446
+ if (path && !isPathInScope(path, paths)) return null;
447
+ const { side, line } = commentLine(comment);
448
+ const resolved = resolvedOf(resolvedById, comment.id);
449
+ const entry = {
450
+ reviewer: reviewerOf(comment),
451
+ body,
452
+ resolved,
453
+ };
454
+ const subject = comment['subject_type'] || 'line';
455
+ const isFile = subject === 'file' || !line;
456
+ if (!isFile && path) {
457
+ const item = findItemForLine(items, path, line, side);
458
+ if (item) {
459
+ const hunk = item.hunk;
460
+ return {
461
+ kind: 'feedback',
462
+ file: itemPath(item) || path,
463
+ oldStart: hunk ? hunk.oldStart : 0,
464
+ newStart: hunk ? hunk.newStart : 0,
465
+ blockId: item.blockId ?? 0,
466
+ origin: item.origin ?? 'pr',
467
+ header: hunk ? hunk.header : '',
468
+ text: formatImportedText(entry),
469
+ done: resolved === true,
470
+ };
471
+ }
472
+ }
473
+ if (path) return toImportedTodo(path, entry);
474
+ return toImportedTodo(PR_TODO_FILE, entry);
475
+ };
476
+
477
+ const summaryToTodo = (record) => {
478
+ if (!isRecord(record)) return null;
479
+ const body = readCommentBody(record.body);
480
+ if (!body) return null;
481
+ return toImportedTodo(PR_TODO_FILE, {
482
+ reviewer: reviewerOf(record),
483
+ body,
484
+ });
485
+ };
486
+
487
+ const discussionToNotes = (discussion, items, paths = []) => {
488
+ const feedback = [];
489
+ const todos = [];
490
+ const resolvedById = discussion.resolvedById ?? new Map();
491
+ const reviewComments = discussion.reviewComments ?? [];
492
+ for (const comment of reviewComments) {
493
+ const note = reviewCommentToNote(comment, items, resolvedById, paths);
494
+ if (!note) continue;
495
+ if (note.kind === 'feedback') feedback.push(note);
496
+ else todos.push(note);
497
+ }
498
+ const reviews = discussion.reviews ?? [];
499
+ for (const review of reviews) {
500
+ const todo = summaryToTodo(review);
501
+ if (todo) todos.push(todo);
502
+ }
503
+ const issueComments = discussion.issueComments ?? [];
504
+ for (const comment of issueComments) {
505
+ const todo = summaryToTodo(comment);
506
+ if (todo) todos.push(todo);
507
+ }
508
+ return { feedback, todos };
509
+ };
510
+
511
+ const loadReviewThreads = async (pr, options) => {
512
+ const threads = [];
513
+ let after = null;
514
+ for (;;) {
515
+ const payload = await requestGraphql(
516
+ REVIEW_THREADS_QUERY,
517
+ {
518
+ owner: pr.owner,
519
+ name: pr.repo,
520
+ number: pr.number,
521
+ after,
522
+ },
523
+ options,
524
+ );
525
+ const data = isRecord(payload.data) ? payload.data : null;
526
+ if (!data) return threads;
527
+ const repository = isRecord(data.repository) ? data.repository : {};
528
+ const pullRequest = isRecord(repository.pullRequest)
529
+ ? repository.pullRequest
530
+ : {};
531
+ const reviewThreads = isRecord(pullRequest.reviewThreads)
532
+ ? pullRequest.reviewThreads
533
+ : {};
534
+ const nodes = Array.isArray(reviewThreads.nodes) ? reviewThreads.nodes : [];
535
+ for (const thread of nodes) threads.push(thread);
536
+ const pageInfo = isRecord(reviewThreads.pageInfo)
537
+ ? reviewThreads.pageInfo
538
+ : {};
539
+ if (!pageInfo.hasNextPage) return threads;
540
+ const cursor = pageInfo.endCursor || null;
541
+ if (!cursor || cursor === after) return threads;
542
+ after = cursor;
543
+ }
544
+ };
545
+
546
+ const threadComments = (thread) => {
547
+ if (!isRecord(thread)) return [];
548
+ const comments = thread.comments;
549
+ if (Array.isArray(comments)) return comments;
550
+ if (isRecord(comments) && Array.isArray(comments.nodes)) {
551
+ return comments.nodes;
552
+ }
553
+ return [];
554
+ };
555
+
556
+ const loadResolvedById = async (pr, options) => {
557
+ const resolved = new Map();
558
+ try {
559
+ const threads = await loadReviewThreads(pr, options);
560
+ for (const thread of threads) {
561
+ const isResolved = thread.isResolved === true;
562
+ for (const comment of threadComments(thread)) {
563
+ if (!isRecord(comment)) continue;
564
+ const id = Number(comment.databaseId);
565
+ if (!Number.isFinite(id)) continue;
566
+ resolved.set(id, isResolved);
567
+ }
568
+ }
569
+ } catch {
570
+ // ignore: resolved state is optional
571
+ }
572
+ return resolved;
573
+ };
574
+
575
+ const loadDiscussion = async (pr, options) => {
576
+ const urls = discussionUrls(pr);
577
+ const reviewComments = await requestGithubList(urls.reviewComments, options);
578
+ const reviews = await requestGithubList(urls.reviews, options);
579
+ const issueComments = await requestGithubList(urls.issueComments, options);
580
+ const resolvedById = await loadResolvedById(pr, options);
581
+ return { reviewComments, reviews, issueComments, resolvedById };
582
+ };
583
+
584
+ const emptyImported = () => ({ feedback: [], todos: [] });
585
+
586
+ const loadImportedNotes = async (pr, items, paths, options) => {
587
+ try {
588
+ const discussion = await loadDiscussion(pr, options);
589
+ return discussionToNotes(discussion, items, paths);
590
+ } catch {
591
+ // ignore: discussion import is optional
592
+ return emptyImported();
593
+ }
594
+ };
595
+
596
+ const loadPullRequest = async (pr, options = {}) => {
597
+ const token = options.token ?? githubToken(options.env);
598
+ const fetchImpl = options.fetch ?? globalThis.fetch;
599
+ const paths = options.paths ?? [];
600
+ const retryOpts = options.retry ?? {};
601
+ const url = prApiUrl(pr);
602
+ const request = { fetch: fetchImpl, token, retry: retryOpts };
603
+ const jsonPage = await requestGithub(url, {
604
+ ...request,
605
+ accept: JSON_ACCEPT,
606
+ });
607
+ const data = parsePullRequestPayload(jsonPage.body);
608
+ const diffPage = await requestGithub(url, {
609
+ ...request,
610
+ accept: DIFF_ACCEPT,
611
+ });
612
+ const parsed = parseDiff(diffPage.body);
613
+ const files = filterChangeFiles(parsed, paths);
614
+ const items = itemsFromFiles(files, 'pr');
615
+ const change = toChange(pr, data, items);
616
+ const sourceLabel = `#${change.number}`;
617
+ const imported = await loadImportedNotes(pr, items, paths, request);
618
+ return { top: options.cwd, items, sourceLabel, change, imported };
619
+ };
620
+
621
+ module.exports = {
622
+ GITHUB_API,
623
+ PR_TODO_FILE,
624
+ parseGithubPrUrl,
625
+ githubToken,
626
+ filterChangeFiles,
627
+ prApiUrl,
628
+ formatImportedText,
629
+ discussionToNotes,
630
+ loadPullRequest,
631
+ };
package/lib/highlight.js CHANGED
@@ -258,7 +258,7 @@ const LOG_DATETIME_RE = new RegExp(
258
258
  '(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?)',
259
259
  );
260
260
 
261
- const LOG_TAG_STYLE = Object.assign(Object.create(null), {
261
+ const LOG_TAG_STYLE = {
262
262
  error: 'logError',
263
263
  err: 'logError',
264
264
  warn: 'logWarn',
@@ -266,24 +266,24 @@ const LOG_TAG_STYLE = Object.assign(Object.create(null), {
266
266
  info: 'logInfo',
267
267
  debug: 'logDebug',
268
268
  log: 'plain',
269
- });
269
+ };
270
270
 
271
- const JSON_LITERAL = Object.assign(Object.create(null), {
271
+ const JSON_LITERAL = {
272
272
  true: 'literal',
273
273
  false: 'literal',
274
274
  null: 'literal',
275
- });
275
+ };
276
276
 
277
- const CSS_NEXT_STYLE = Object.assign(Object.create(null), {
277
+ const CSS_NEXT_STYLE = {
278
278
  ':': 'property',
279
279
  '{': 'tag',
280
- });
280
+ };
281
281
 
282
- const EXPECT_STYLE = Object.assign(Object.create(null), {
282
+ const EXPECT_STYLE = {
283
283
  function: 'function',
284
284
  class: 'className',
285
285
  type: 'className',
286
- });
286
+ };
287
287
 
288
288
  const NEXT_EXPECT = {
289
289
  function: 'function',
@@ -299,28 +299,25 @@ const wordStyle = (words, style) => {
299
299
  return table;
300
300
  };
301
301
 
302
- const BASH_WORD_STYLE = Object.assign(
303
- Object.create(null),
304
- wordStyle(BASH_KEYWORDS, 'control'),
305
- wordStyle(BASH_BUILTINS, 'function'),
306
- );
302
+ const BASH_WORD_STYLE = {
303
+ ...wordStyle(BASH_KEYWORDS, 'control'),
304
+ ...wordStyle(BASH_BUILTINS, 'function'),
305
+ };
307
306
 
308
307
  const typeOverlay = TYPES.filter((word) => STORAGE.includes(word));
309
308
 
310
- const WORD_STYLE = Object.assign(
311
- Object.create(null),
312
- wordStyle(CONTROL, 'control'),
313
- wordStyle(LITERALS, 'literal'),
314
- wordStyle(BUILTINS, 'className'),
315
- wordStyle(STORAGE, 'storage'),
316
- wordStyle(typeOverlay, 'type'),
317
- );
309
+ const WORD_STYLE = {
310
+ ...wordStyle(CONTROL, 'control'),
311
+ ...wordStyle(LITERALS, 'literal'),
312
+ ...wordStyle(BUILTINS, 'className'),
313
+ ...wordStyle(STORAGE, 'storage'),
314
+ ...wordStyle(typeOverlay, 'type'),
315
+ };
318
316
 
319
- const TS_WORD_STYLE = Object.assign(
320
- Object.create(null),
321
- WORD_STYLE,
322
- wordStyle(TYPES, 'type'),
323
- );
317
+ const TS_WORD_STYLE = {
318
+ ...WORD_STYLE,
319
+ ...wordStyle(TYPES, 'type'),
320
+ };
324
321
 
325
322
  const peekIdent = (source, i) => {
326
323
  if (!/[A-Za-z_$]/.test(source[i] || '')) return null;