docusaurus-plugin-glossary 1.1.2 → 1.3.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.
@@ -1,10 +1,10 @@
1
- const visit = require('unist-util-visit');
2
- const path = require('path');
3
- const fs = require('fs');
1
+ import { visit } from 'unist-util-visit';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
4
 
5
5
  /**
6
6
  * Creates a remark plugin that automatically detects and replaces glossary terms in markdown
7
- *
7
+ *
8
8
  * @param {object} options - Plugin options
9
9
  * @param {Array} options.terms - Array of glossary term objects with {term, definition}
10
10
  * @param {string} options.glossaryPath - Path to glossary JSON file (optional, if terms not provided)
@@ -12,11 +12,11 @@ const fs = require('fs');
12
12
  * @param {string} options.siteDir - Docusaurus site directory (required if using glossaryPath)
13
13
  * @returns {function} Remark plugin function
14
14
  */
15
- function remarkGlossaryTerms({
16
- terms = [],
15
+ export default function remarkGlossaryTerms({
16
+ terms = [],
17
17
  glossaryPath = null,
18
18
  routePath = '/glossary',
19
- siteDir = null
19
+ siteDir = null,
20
20
  } = {}) {
21
21
  let glossaryTerms = terms;
22
22
 
@@ -44,13 +44,11 @@ function remarkGlossaryTerms({
44
44
 
45
45
  // Sort terms by length (longest first) to avoid partial matches
46
46
  // e.g., "Application Programming Interface" should match before "API"
47
- const sortedTerms = Array.from(termMap.entries()).sort(
48
- (a, b) => b[0].length - a[0].length
49
- );
47
+ const sortedTerms = Array.from(termMap.entries()).sort((a, b) => b[0].length - a[0].length);
50
48
 
51
49
  // If no terms, return a no-op transformer
52
50
  if (sortedTerms.length === 0) {
53
- return (tree) => tree;
51
+ return tree => tree;
54
52
  }
55
53
 
56
54
  /**
@@ -71,29 +69,50 @@ function remarkGlossaryTerms({
71
69
  for (const [lowerTerm, termObj] of sortedTerms) {
72
70
  const term = termObj.term;
73
71
  let searchIndex = 0;
74
-
72
+
75
73
  while (searchIndex < textLower.length) {
76
74
  const index = textLower.indexOf(lowerTerm, searchIndex);
77
75
  if (index === -1) break;
78
76
 
79
- // Check if it's a whole word match
77
+ // Check if it's a whole word match, with simple plural tolerance ('s' or 'es')
80
78
  const beforeChar = index > 0 ? textLower[index - 1] : ' ';
81
- const afterChar = index + lowerTerm.length < textLower.length
82
- ? textLower[index + lowerTerm.length]
83
- : ' ';
84
-
85
- // Word boundary check (alphanumeric characters)
86
- const isWordBoundary =
87
- !/\w/.test(beforeChar) && !/\w/.test(afterChar);
79
+ const afterIndex = index + lowerTerm.length;
80
+ const afterChar = afterIndex < textLower.length ? textLower[afterIndex] : ' ';
81
+
82
+ let matchLength = term.length;
83
+ let isWordBoundary = !/\w/.test(beforeChar) && !/\w/.test(afterChar);
84
+
85
+ // Allow trailing 's' plural (e.g., webhook -> webhooks)
86
+ if (!isWordBoundary && afterChar === 's') {
87
+ const nextChar = afterIndex + 1 < textLower.length ? textLower[afterIndex + 1] : ' ';
88
+ if (!/\w/.test(nextChar)) {
89
+ isWordBoundary = true;
90
+ matchLength = term.length + 1;
91
+ }
92
+ }
93
+
94
+ // Allow trailing 'es' plural (e.g., API -> APIs, box -> boxes)
95
+ if (
96
+ !isWordBoundary &&
97
+ afterChar === 'e' &&
98
+ afterIndex + 1 < textLower.length &&
99
+ textLower[afterIndex + 1] === 's'
100
+ ) {
101
+ const nextChar = afterIndex + 2 < textLower.length ? textLower[afterIndex + 2] : ' ';
102
+ if (!/\w/.test(nextChar)) {
103
+ isWordBoundary = true;
104
+ matchLength = term.length + 2;
105
+ }
106
+ }
88
107
 
89
108
  if (isWordBoundary) {
90
109
  matches.push({
91
110
  index,
92
- length: term.length,
111
+ length: matchLength,
93
112
  term: term,
94
113
  termObj: termObj,
95
114
  // Store original case from the text
96
- originalText: text.substring(index, index + term.length)
115
+ originalText: text.substring(index, index + matchLength),
97
116
  });
98
117
  }
99
118
 
@@ -120,7 +139,7 @@ function remarkGlossaryTerms({
120
139
  if (match.index > lastIndex) {
121
140
  result.push({
122
141
  type: 'text',
123
- value: text.substring(lastIndex, match.index)
142
+ value: text.substring(lastIndex, match.index),
124
143
  });
125
144
  }
126
145
 
@@ -132,25 +151,25 @@ function remarkGlossaryTerms({
132
151
  {
133
152
  type: 'mdxJsxAttribute',
134
153
  name: 'term',
135
- value: match.termObj.term
154
+ value: match.termObj.term,
136
155
  },
137
156
  {
138
157
  type: 'mdxJsxAttribute',
139
158
  name: 'definition',
140
- value: match.termObj.definition || ''
159
+ value: match.termObj.definition || '',
141
160
  },
142
161
  {
143
162
  type: 'mdxJsxAttribute',
144
163
  name: 'routePath',
145
- value: routePath
146
- }
164
+ value: routePath,
165
+ },
147
166
  ],
148
167
  children: [
149
168
  {
150
169
  type: 'text',
151
- value: match.originalText
152
- }
153
- ]
170
+ value: match.originalText,
171
+ },
172
+ ],
154
173
  });
155
174
 
156
175
  lastIndex = match.index + match.length;
@@ -160,14 +179,14 @@ function remarkGlossaryTerms({
160
179
  if (lastIndex < text.length) {
161
180
  result.push({
162
181
  type: 'text',
163
- value: text.substring(lastIndex)
182
+ value: text.substring(lastIndex),
164
183
  });
165
184
  }
166
185
 
167
186
  return result.length > 0 ? result : [{ type: 'text', value: text }];
168
187
  }
169
188
 
170
- return (tree) => {
189
+ return tree => {
171
190
  let usedGlossaryTerm = false;
172
191
  visit(tree, 'text', (node, index, parent) => {
173
192
  // Skip text nodes inside code blocks, links, or existing MDX components
@@ -185,8 +204,10 @@ function remarkGlossaryTerms({
185
204
  const replacements = replaceTermsInText(node.value);
186
205
 
187
206
  // If we have replacements, replace the single text node with multiple nodes
188
- if (replacements.length > 1 ||
189
- (replacements.length === 1 && replacements[0].type !== 'text')) {
207
+ if (
208
+ replacements.length > 1 ||
209
+ (replacements.length === 1 && replacements[0].type !== 'text')
210
+ ) {
190
211
  // Convert to text elements for paragraph context if needed
191
212
  const newNodes = replacements.map(replacement => {
192
213
  if (replacement.type === 'mdxJsxFlowElement') {
@@ -196,7 +217,7 @@ function remarkGlossaryTerms({
196
217
  type: 'mdxJsxTextElement',
197
218
  name: replacement.name,
198
219
  attributes: replacement.attributes,
199
- children: replacement.children
220
+ children: replacement.children,
200
221
  };
201
222
  }
202
223
  }
@@ -217,9 +238,11 @@ function remarkGlossaryTerms({
217
238
 
218
239
  // Inject MDX import for GlossaryTerm if we used it anywhere in this file
219
240
  if (usedGlossaryTerm) {
241
+ // Create import node matching MDX v3 expectations
242
+ // Both 'value' (the import string) and 'data.estree' (the parsed AST) are required
220
243
  const importNode = {
221
244
  type: 'mdxjsEsm',
222
- value: "import GlossaryTerm from '@theme/GlossaryTerm';",
245
+ value: "import GlossaryTerm from '@theme/GlossaryTerm'",
223
246
  data: {
224
247
  estree: {
225
248
  type: 'Program',
@@ -230,26 +253,52 @@ function remarkGlossaryTerms({
230
253
  specifiers: [
231
254
  {
232
255
  type: 'ImportDefaultSpecifier',
233
- local: { type: 'Identifier', name: 'GlossaryTerm' }
234
- }
256
+ local: { type: 'Identifier', name: 'GlossaryTerm' },
257
+ },
235
258
  ],
236
- source: { type: 'Literal', value: '@theme/GlossaryTerm' }
237
- }
238
- ]
239
- }
240
- }
259
+ source: {
260
+ type: 'Literal',
261
+ value: '@theme/GlossaryTerm',
262
+ raw: "'@theme/GlossaryTerm'",
263
+ },
264
+ },
265
+ ],
266
+ },
267
+ },
241
268
  };
242
269
 
243
270
  // Avoid duplicate imports if already present
244
- const hasExistingImport = Array.isArray(tree.children) && tree.children.some(
245
- (n) => n.type === 'mdxjsEsm' && typeof n.value === 'string' && n.value.includes("@theme/GlossaryTerm")
246
- );
271
+ const hasExistingImport =
272
+ Array.isArray(tree.children) &&
273
+ tree.children.some(n => {
274
+ if (n.type !== 'mdxjsEsm') return false;
275
+ // Check value string (for older MDX format)
276
+ if (typeof n.value === 'string' && n.value.includes('@theme/GlossaryTerm')) {
277
+ return true;
278
+ }
279
+ // Check estree data (for newer MDX format)
280
+ if (n.data?.estree?.body) {
281
+ return n.data.estree.body.some(
282
+ stmt =>
283
+ stmt.type === 'ImportDeclaration' && stmt.source?.value === '@theme/GlossaryTerm'
284
+ );
285
+ }
286
+ return false;
287
+ });
288
+
247
289
  if (!hasExistingImport) {
290
+ // Place import at the very beginning of the file (before all other nodes)
291
+ // This ensures it's available when MDX compiles the JSX elements
292
+ if (!Array.isArray(tree.children)) {
293
+ tree.children = [];
294
+ }
295
+ // Insert at the very beginning (index 0) to ensure it's processed first
248
296
  tree.children.unshift(importNode);
297
+ // Debug: verify import was added (remove in production)
298
+ if (process.env.NODE_ENV !== 'production') {
299
+ console.log('[glossary-plugin] Injected GlossaryTerm import');
300
+ }
249
301
  }
250
302
  }
251
303
  };
252
304
  }
253
-
254
- module.exports = remarkGlossaryTerms;
255
-
@@ -0,0 +1,130 @@
1
+ import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
2
+ import { usePluginData } from '@docusaurus/useGlobalData';
3
+ import styles from './styles.module.css';
4
+
5
+ /**
6
+ * GlossaryTerm component - displays an inline term with tooltip
7
+ *
8
+ * Usage:
9
+ * import GlossaryTerm from '@theme/GlossaryTerm';
10
+ *
11
+ * <GlossaryTerm term="API" definition="Application Programming Interface" />
12
+ * or
13
+ * <GlossaryTerm term="API">custom display text</GlossaryTerm>
14
+ *
15
+ * @param {object} props
16
+ * @param {string} props.term - The glossary term
17
+ * @param {string} props.definition - The definition to show in tooltip
18
+ * @param {string} props.routePath - Route path to glossary page (default: '/glossary')
19
+ * @param {React.ReactNode} props.children - Optional custom display text
20
+ */
21
+ export default function GlossaryTerm({ term, definition, routePath = '/glossary', children }) {
22
+ const [showTooltip, setShowTooltip] = useState(false);
23
+ const [tooltipStyle, setTooltipStyle] = useState(null);
24
+ const [placement, setPlacement] = useState('top'); // 'top' | 'bottom'
25
+ const wrapperRef = useRef(null);
26
+ const tooltipRef = useRef(null);
27
+
28
+ const updatePosition = useCallback(() => {
29
+ if (!wrapperRef.current || !tooltipRef.current) return;
30
+ const wrapperRect = wrapperRef.current.getBoundingClientRect();
31
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
32
+
33
+ const viewportWidth = window.innerWidth;
34
+ const viewportHeight = window.innerHeight;
35
+
36
+ const preferredGap = 8; // px
37
+
38
+ // Decide top vs bottom based on available space
39
+ const hasSpaceAbove = wrapperRect.top >= tooltipRect.height + preferredGap;
40
+ const hasSpaceBelow = viewportHeight - wrapperRect.bottom >= tooltipRect.height + preferredGap;
41
+ const nextPlacement = hasSpaceAbove || !hasSpaceBelow ? 'top' : 'bottom';
42
+
43
+ let top;
44
+ if (nextPlacement === 'top') {
45
+ top = wrapperRect.top - tooltipRect.height - preferredGap;
46
+ } else {
47
+ top = wrapperRect.bottom + preferredGap;
48
+ }
49
+
50
+ // Center horizontally on the wrapper, then clamp within viewport with margin
51
+ const horizontalMargin = 8;
52
+ let left = wrapperRect.left + wrapperRect.width / 2 - tooltipRect.width / 2;
53
+ left = Math.max(
54
+ horizontalMargin,
55
+ Math.min(left, viewportWidth - tooltipRect.width - horizontalMargin)
56
+ );
57
+
58
+ setPlacement(nextPlacement);
59
+ setTooltipStyle({ top: Math.max(4, top), left });
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ if (!showTooltip) return;
64
+ updatePosition();
65
+ const onScroll = () => updatePosition();
66
+ const onResize = () => updatePosition();
67
+ window.addEventListener('scroll', onScroll, true);
68
+ window.addEventListener('resize', onResize);
69
+ return () => {
70
+ window.removeEventListener('scroll', onScroll, true);
71
+ window.removeEventListener('resize', onResize);
72
+ };
73
+ }, [showTooltip, updatePosition]);
74
+
75
+ // Pull definition/route from plugin global data if not provided
76
+ const pluginData = usePluginData('docusaurus-plugin-glossary');
77
+ const effectiveDefinition = useMemo(() => {
78
+ if (definition && typeof definition === 'string' && definition.length > 0) {
79
+ return definition;
80
+ }
81
+ const terms = (pluginData && pluginData.terms) || [];
82
+ const found = terms.find(
83
+ t => typeof t.term === 'string' && t.term.toLowerCase() === String(term).toLowerCase()
84
+ );
85
+ return found && found.definition ? found.definition : undefined;
86
+ }, [definition, pluginData, term]);
87
+
88
+ const effectiveRoutePath = useMemo(() => {
89
+ if (routePath && typeof routePath === 'string' && routePath.length > 0) return routePath;
90
+ return (pluginData && pluginData.routePath) || '/glossary';
91
+ }, [pluginData, routePath]);
92
+
93
+ const displayText = children || term;
94
+ const termId = term.toLowerCase().replace(/\s+/g, '-');
95
+
96
+ return (
97
+ <span ref={wrapperRef} className={styles.glossaryTermWrapper}>
98
+ <a
99
+ href={`${effectiveRoutePath}#${termId}`}
100
+ className={styles.glossaryTerm}
101
+ onMouseEnter={() => setShowTooltip(true)}
102
+ onMouseLeave={() => setShowTooltip(false)}
103
+ onFocus={() => setShowTooltip(true)}
104
+ onBlur={() => setShowTooltip(false)}
105
+ aria-describedby={`tooltip-${termId}`}
106
+ >
107
+ {displayText}
108
+ </a>
109
+ {effectiveDefinition && (
110
+ <span
111
+ ref={tooltipRef}
112
+ id={`tooltip-${termId}`}
113
+ className={
114
+ `${styles.tooltip} ${showTooltip ? styles.tooltipVisible : ''} ` +
115
+ `${placement === 'top' ? styles.tooltipTop : styles.tooltipBottom} ` +
116
+ `${styles.tooltipFloating}`
117
+ }
118
+ role="tooltip"
119
+ style={
120
+ showTooltip && tooltipStyle
121
+ ? { top: `${tooltipStyle.top}px`, left: `${tooltipStyle.left}px` }
122
+ : undefined
123
+ }
124
+ >
125
+ <strong>{term}:</strong> {effectiveDefinition}
126
+ </span>
127
+ )}
128
+ </span>
129
+ );
130
+ }
@@ -113,4 +113,23 @@ describe('GlossaryTerm', () => {
113
113
  const tooltip = screen.getByRole('tooltip');
114
114
  expect(tooltip).toHaveAttribute('id', 'tooltip-api');
115
115
  });
116
+
117
+ it('positions tooltip within viewport and adds placement classes', async () => {
118
+ const user = userEvent.setup();
119
+ render(<GlossaryTerm term="Edge" definition="Near the boundary of the viewport" />);
120
+
121
+ const link = screen.getByRole('link');
122
+ await user.hover(link);
123
+
124
+ const tooltip = screen.getByRole('tooltip');
125
+ expect(tooltip).toHaveClass('tooltipVisible');
126
+ expect(tooltip).toHaveClass('tooltipFloating');
127
+ // One of the placement classes should be present
128
+ const hasPlacement =
129
+ tooltip.classList.contains('tooltipTop') || tooltip.classList.contains('tooltipBottom');
130
+ expect(hasPlacement).toBe(true);
131
+ // Inline style should include computed top/left
132
+ expect(tooltip.style.top).toMatch(/px$/);
133
+ expect(tooltip.style.left).toMatch(/px$/);
134
+ });
116
135
  });
@@ -4,17 +4,19 @@
4
4
  }
5
5
 
6
6
  .glossaryTerm {
7
- color: var(--ifm-color-primary);
7
+ color: var(--ifm-font-color-base);
8
8
  text-decoration: none;
9
- border-bottom: 1px dotted var(--ifm-color-primary);
9
+ border-bottom: 1px dotted;
10
+ border-bottom-color: color-mix(in srgb, var(--ifm-font-color-base) 20%, transparent);
10
11
  cursor: help;
11
- font-weight: 500;
12
12
  transition: all 0.2s;
13
- }
13
+ }
14
14
 
15
15
  .glossaryTerm:hover {
16
16
  color: var(--ifm-color-primary-dark);
17
17
  border-bottom-style: solid;
18
+ border-bottom-color: var(--ifm-color-primary-dark);
19
+ border-bottom: none;
18
20
  }
19
21
 
20
22
  .tooltip {
@@ -40,25 +42,55 @@
40
42
  visibility: hidden;
41
43
  }
42
44
 
43
- .tooltip::after {
45
+ /* When we compute viewport-safe placement via JS */
46
+ .tooltipFloating {
47
+ position: fixed;
48
+ bottom: auto;
49
+ transform: none;
50
+ left: 0; /* overridden inline */
51
+ top: 0; /* overridden inline */
52
+ }
53
+
54
+ /* Base arrow removed; arrows are defined per-placement to avoid inversion */
55
+
56
+ /* Arrow for top placement (tooltip above anchor) */
57
+ .tooltipTop::after {
44
58
  content: '';
45
59
  position: absolute;
46
- top: 100%;
47
60
  left: 50%;
48
61
  transform: translateX(-50%);
62
+ bottom: -6px;
49
63
  border: 6px solid transparent;
50
64
  border-top-color: var(--ifm-background-surface-color);
51
65
  }
52
-
53
- .tooltip::before {
66
+ .tooltipTop::before {
54
67
  content: '';
55
68
  position: absolute;
56
- top: 100%;
57
69
  left: 50%;
58
70
  transform: translateX(-50%);
71
+ bottom: -7px;
59
72
  border: 7px solid transparent;
60
73
  border-top-color: var(--ifm-color-emphasis-300);
61
- margin-top: 1px;
74
+ }
75
+
76
+ /* Arrow for bottom placement (tooltip below anchor) */
77
+ .tooltipBottom::after {
78
+ content: '';
79
+ position: absolute;
80
+ left: 50%;
81
+ transform: translateX(-50%);
82
+ top: -6px;
83
+ border: 6px solid transparent;
84
+ border-bottom-color: var(--ifm-background-surface-color);
85
+ }
86
+ .tooltipBottom::before {
87
+ content: '';
88
+ position: absolute;
89
+ left: 50%;
90
+ transform: translateX(-50%);
91
+ top: -7px;
92
+ border: 7px solid transparent;
93
+ border-bottom-color: var(--ifm-color-emphasis-300);
62
94
  }
63
95
 
64
96
  .tooltipVisible {
@@ -79,13 +111,19 @@
79
111
  border-color: var(--ifm-color-emphasis-400);
80
112
  }
81
113
 
82
- [data-theme='dark'] .tooltip::after {
114
+ /* Dark mode arrow overrides scoped to placement to avoid upside-down arrows */
115
+ [data-theme='dark'] .tooltipTop::after {
83
116
  border-top-color: var(--ifm-background-surface-color);
84
117
  }
85
-
86
- [data-theme='dark'] .tooltip::before {
118
+ [data-theme='dark'] .tooltipTop::before {
87
119
  border-top-color: var(--ifm-color-emphasis-400);
88
120
  }
121
+ [data-theme='dark'] .tooltipBottom::after {
122
+ border-bottom-color: var(--ifm-background-surface-color);
123
+ }
124
+ [data-theme='dark'] .tooltipBottom::before {
125
+ border-bottom-color: var(--ifm-color-emphasis-400);
126
+ }
89
127
 
90
128
  /* Responsive adjustments */
91
129
  @media (max-width: 768px) {
package/package.json CHANGED
@@ -1,24 +1,31 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-glossary",
3
- "version": "1.1.2",
3
+ "version": "1.3.1",
4
4
  "description": "A Docusaurus plugin for creating and managing glossary terms with auto-generated pages and inline tooltips",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./lib/index.js"
10
+ },
11
+ "./remark/glossary-terms": "./lib/remark/glossary-terms.js"
12
+ },
6
13
  "files": [
7
- "index.js",
8
- "components/",
9
- "theme/",
10
- "remark/",
14
+ "lib/",
11
15
  "README.md",
12
16
  "LICENSE"
13
17
  ],
14
18
  "scripts": {
19
+ "build": "tsc && node scripts/build.js",
20
+ "watch": "node scripts/watch.js",
15
21
  "test": "jest",
16
22
  "test:watch": "jest --watch",
17
23
  "test:coverage": "jest --coverage",
18
24
  "example:start": "npm --prefix examples/docusaurus-v3 run start",
19
25
  "example:build": "npm --prefix examples/docusaurus-v3 run build",
20
26
  "example:serve": "npm --prefix examples/docusaurus-v3 run serve",
21
- "prepublishOnly": "npm test",
27
+ "example:clear": "npm --prefix examples/docusaurus-v3 run clear",
28
+ "prepublishOnly": "npm run build && npm test",
22
29
  "version": "npm version",
23
30
  "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
24
31
  "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\""
@@ -49,7 +56,8 @@
49
56
  },
50
57
  "dependencies": {
51
58
  "fs-extra": "^11.0.0",
52
- "unist-util-visit": "^5.0.0"
59
+ "unist-util-visit": "^5.0.0",
60
+ "validate-peer-dependencies": "^2.2.0"
53
61
  },
54
62
  "engines": {
55
63
  "node": ">=16.14"
@@ -57,6 +65,8 @@
57
65
  "devDependencies": {
58
66
  "@babel/preset-env": "^7.28.5",
59
67
  "@babel/preset-react": "^7.28.5",
68
+ "@docusaurus/tsconfig": "^3.9.2",
69
+ "@docusaurus/types": "^3.9.2",
60
70
  "@testing-library/jest-dom": "^6.9.1",
61
71
  "@testing-library/react": "^16.3.0",
62
72
  "@testing-library/user-event": "^14.6.1",
@@ -64,6 +74,19 @@
64
74
  "identity-obj-proxy": "^3.0.0",
65
75
  "jest": "^30.2.0",
66
76
  "jest-environment-jsdom": "^30.2.0",
67
- "prettier": "^3.6.2"
77
+ "prettier": "^3.6.2",
78
+ "typescript": "^5.7.3"
79
+ },
80
+ "browser": {
81
+ "path": false,
82
+ "url": false,
83
+ "fs": false,
84
+ "fs-extra": false,
85
+ "graceful-fs": false,
86
+ "jsonfile": false,
87
+ "util": false,
88
+ "assert": false,
89
+ "stream": false,
90
+ "constants": false
68
91
  }
69
92
  }