extwee 2.3.1 → 2.3.3

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 (43) hide show
  1. package/build/extwee.core.min.js +1 -0
  2. package/build/extwee.twine1html.min.js +1 -0
  3. package/build/extwee.twine2archive.min.js +1 -0
  4. package/build/extwee.tws.min.js +1 -0
  5. package/build/test-modular.html +126 -0
  6. package/eslint.config.js +4 -1
  7. package/package.json +20 -17
  8. package/src/IFID/generate.js +2 -2
  9. package/src/Story.js +1 -1
  10. package/src/Twine1HTML/parse-web.js +255 -0
  11. package/src/Twine2ArchiveHTML/parse-web.js +134 -0
  12. package/src/Twine2HTML/parse-web.js +428 -0
  13. package/src/Web/web-core.js +31 -0
  14. package/src/Web/web-index.js +31 -0
  15. package/src/Web/web-twine1html.js +15 -0
  16. package/src/Web/web-twine2archive.js +15 -0
  17. package/src/Web/web-tws.js +12 -0
  18. package/test/Config/Config.test.js +1 -1
  19. package/test/Config/isDirectory.test.js +15 -9
  20. package/test/Config/isFile.test.js +14 -11
  21. package/test/Config/loadStoryFormat.test.js +49 -33
  22. package/test/Config/readDirectories.test.js +25 -15
  23. package/test/Objects/Story.test.js +1 -0
  24. package/test/StoryFormat/StoryFormat.Parse.test.js +1 -0
  25. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +1 -0
  26. package/test/Twine2HTML/Twine2HTML.Parse.test.js +1 -0
  27. package/test/Web/window.Extwee.test.js +20 -13
  28. package/types/src/Story.d.ts +1 -1
  29. package/types/src/Twine1HTML/parse-web.d.ts +10 -0
  30. package/types/src/Twine2ArchiveHTML/parse-web.d.ts +37 -0
  31. package/types/src/Twine2HTML/parse-web.d.ts +21 -0
  32. package/types/src/Web/html-entities-lite.d.ts +12 -0
  33. package/types/src/Web/semver-lite.d.ts +10 -0
  34. package/types/src/Web/uuid-lite.d.ts +6 -0
  35. package/types/src/Web/web-core.d.ts +1 -0
  36. package/types/src/Web/web-index.d.ts +1 -0
  37. package/types/src/Web/web-twine1html.d.ts +3 -0
  38. package/types/src/Web/web-twine2archive.d.ts +3 -0
  39. package/types/src/Web/web-tws.d.ts +2 -0
  40. package/webpack.config.js +22 -2
  41. package/build/extwee.web.min.js +0 -2
  42. package/build/extwee.web.min.js.LICENSE.txt +0 -1
  43. package/web-index.js +0 -31
@@ -0,0 +1,428 @@
1
+ import { Story } from '../Story.js';
2
+ import Passage from '../Passage.js';
3
+ import { decode } from 'html-entities';
4
+
5
+ /**
6
+ * Lightweight HTML parser for web builds - specifically for Twine 2 HTML parsing
7
+ * This replaces node-html-parser to reduce bundle size
8
+ */
9
+ class LightweightTwine2Parser {
10
+ constructor(html) {
11
+ this.html = html;
12
+ this.doc = null;
13
+
14
+ // Parse HTML using browser's native DOMParser if available, otherwise fallback
15
+ if (typeof DOMParser !== 'undefined') {
16
+ const parser = new DOMParser();
17
+ this.doc = parser.parseFromString(html, 'text/html');
18
+ } else {
19
+ // Fallback for environments without DOMParser
20
+ this.doc = this.createSimpleDOM(html);
21
+ }
22
+ }
23
+
24
+ getElementsByTagName(tagName) {
25
+ if (this.doc && this.doc.getElementsByTagName) {
26
+ return Array.from(this.doc.getElementsByTagName(tagName));
27
+ }
28
+
29
+ // Fallback implementation
30
+ if (tagName === 'tw-storydata') {
31
+ return this.extractStoryDataElements();
32
+ }
33
+ if (tagName === 'tw-passagedata') {
34
+ return this.extractPassageDataElements();
35
+ }
36
+ if (tagName === 'style') {
37
+ return this.extractStyleElements();
38
+ }
39
+ return [];
40
+ }
41
+
42
+ extractStoryDataElements() {
43
+ const storyDataRegex = /<tw-storydata[^>]*>([\s\S]*?)<\/tw-storydata>/gi;
44
+ const elements = [];
45
+ let match;
46
+
47
+ while ((match = storyDataRegex.exec(this.html)) !== null) {
48
+ const elementHtml = match[0];
49
+ const attributes = this.parseAttributes(elementHtml);
50
+ const innerHTML = match[1];
51
+
52
+ elements.push({
53
+ attributes,
54
+ innerHTML,
55
+ rawText: innerHTML
56
+ });
57
+ }
58
+
59
+ return elements;
60
+ }
61
+
62
+ extractPassageDataElements() {
63
+ const passageDataRegex = /<tw-passagedata[^>]*>([\s\S]*?)<\/tw-passagedata>/gi;
64
+ const elements = [];
65
+ let match;
66
+
67
+ while ((match = passageDataRegex.exec(this.html)) !== null) {
68
+ const elementHtml = match[0];
69
+ const attributes = this.parseAttributes(elementHtml);
70
+ const textContent = this.extractTextContent(match[1]);
71
+
72
+ elements.push({
73
+ attributes,
74
+ rawText: textContent
75
+ });
76
+ }
77
+
78
+ return elements;
79
+ }
80
+
81
+ extractStyleElements() {
82
+ const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
83
+ const elements = [];
84
+ let match;
85
+
86
+ while ((match = styleRegex.exec(this.html)) !== null) {
87
+ const elementHtml = match[0];
88
+ const attributes = this.parseAttributes(elementHtml);
89
+ const textContent = match[1];
90
+
91
+ elements.push({
92
+ attributes,
93
+ rawText: textContent,
94
+ innerHTML: textContent
95
+ });
96
+ }
97
+
98
+ return elements;
99
+ }
100
+
101
+ parseAttributes(elementHtml) {
102
+ const attributes = {};
103
+
104
+ // Common attribute patterns
105
+ const attributeRegex = /(\w+(?:-\w+)*)=["']([^"']*)["']/g;
106
+ let match;
107
+
108
+ while ((match = attributeRegex.exec(elementHtml)) !== null) {
109
+ attributes[match[1]] = match[2];
110
+ }
111
+
112
+ return attributes;
113
+ }
114
+
115
+ extractTextContent(html) {
116
+ // Remove HTML tags and decode basic entities
117
+ return html
118
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
119
+ .replace(/&lt;/g, '<')
120
+ .replace(/&gt;/g, '>')
121
+ .replace(/&quot;/g, '"')
122
+ .replace(/&#39;/g, "'")
123
+ .replace(/&amp;/g, '&') // This should be last
124
+ .trim();
125
+ }
126
+
127
+ // eslint-disable-next-line no-unused-vars
128
+ createSimpleDOM(_html) {
129
+ // Minimal DOM-like object for fallback
130
+ return {
131
+ getElementsByTagName: (tagName) => {
132
+ if (tagName === 'tw-storydata') {
133
+ return this.extractStoryDataElements();
134
+ }
135
+ if (tagName === 'tw-passagedata') {
136
+ return this.extractPassageDataElements();
137
+ }
138
+ if (tagName === 'style') {
139
+ return this.extractStyleElements();
140
+ }
141
+ return [];
142
+ }
143
+ };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Web-optimized Twine 2 HTML parser with reduced dependencies
149
+ * Parse Twine 2 HTML into Story object using lightweight DOM parsing
150
+ *
151
+ * See: Twine 2 HTML Output Specification
152
+ * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md)
153
+ *
154
+ * Produces warnings for:
155
+ * - Missing name attribute on `<tw-storydata>` element.
156
+ * - Missing IFID attribute on `<tw-storydata>` element.
157
+ * - Malformed IFID attribute on `<tw-storydata>` element.
158
+ * @function parse
159
+ * @param {string} content - Twine 2 HTML content to parse.
160
+ * @returns {Story} Story object based on Twine 2 HTML content.
161
+ * @throws {TypeError} Content is not a string.
162
+ * @throws {Error} Not Twine 2 HTML content!
163
+ * @throws {Error} Cannot parse passage data without name!
164
+ * @throws {Error} Passages are required to have PID!
165
+ */
166
+ function parse(content) {
167
+ // Create new story.
168
+ const story = new Story();
169
+
170
+ // Can only parse string values.
171
+ if (typeof content !== 'string') {
172
+ throw new TypeError('TypeError: Content is not a string!');
173
+ }
174
+
175
+ // Set default start node.
176
+ let startNode = null;
177
+
178
+ // Use lightweight parser for web builds
179
+ const dom = new LightweightTwine2Parser(content);
180
+
181
+ // Pull out the `<tw-storydata>` element.
182
+ const storyDataElements = dom.getElementsByTagName('tw-storydata');
183
+
184
+ // Did we find any elements?
185
+ if (storyDataElements.length === 0) {
186
+ // If there is not a single `<tw-storydata>` element, this is not a Twine 2 story!
187
+ throw new TypeError('TypeError: Not Twine 2 HTML content!');
188
+ }
189
+
190
+ // We only parse the first element found.
191
+ const storyData = storyDataElements[0];
192
+
193
+ /**
194
+ * name: (string) Required.
195
+ * The name of the story.
196
+ */
197
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
198
+ // Set the story name
199
+ story.name = storyData.attributes.name;
200
+ } else {
201
+ // Name is a required field. Warn user.
202
+ console.warn('Warning: The name attribute is missing from tw-storydata!');
203
+ }
204
+
205
+ /**
206
+ * ifid: (string) Required.
207
+ * An IFID is a sequence of between 8 and 63 characters,
208
+ * each of which shall be a digit, a capital letter or a
209
+ * hyphen that uniquely identify a story (see Treaty of Babel).
210
+ */
211
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'ifid')) {
212
+ // Update story IFID.
213
+ story.IFID = storyData.attributes.ifid;
214
+ } else {
215
+ // Name is a required filed. Warn user.
216
+ console.warn('Warning: The ifid attribute is missing from tw-storydata!');
217
+ }
218
+
219
+ // Check if the IFID has valid formatting.
220
+ if (story.IFID.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/) === null) {
221
+ // IFID is not valid.
222
+ console.warn('Warning: The IFID is not in valid UUIDv4 formatting on tw-storydata!');
223
+ }
224
+
225
+ /**
226
+ * creator: (string) Optional.
227
+ * The name of program used to create the file.
228
+ */
229
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator')) {
230
+ // Update story creator
231
+ story.creator = storyData.attributes.creator;
232
+ }
233
+
234
+ /**
235
+ * creator-version: (string) Optional.
236
+ * The version of program used to create the file.
237
+ */
238
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator-version')) {
239
+ // Update story creator version
240
+ story.creatorVersion = storyData.attributes['creator-version'];
241
+ }
242
+
243
+ /**
244
+ * format: (string) Optional.
245
+ * The story format used when publishing file.
246
+ */
247
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format')) {
248
+ // Update story format
249
+ story.format = storyData.attributes.format;
250
+ }
251
+
252
+ /**
253
+ * format-version: (string) Optional.
254
+ * The version of story format used when publishing file.
255
+ */
256
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format-version')) {
257
+ // Update story format version
258
+ story.formatVersion = storyData.attributes['format-version'];
259
+ }
260
+
261
+ /**
262
+ * startnode: (string) Optional.
263
+ * The PID of the starting passage.
264
+ */
265
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'startnode')) {
266
+ // Update start node
267
+ startNode = storyData.attributes.startnode;
268
+ }
269
+
270
+ /**
271
+ * zoom: (string) Optional.
272
+ * Zoom level between 0.25 (25%) and 4.0 (400%).
273
+ */
274
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'zoom')) {
275
+ // Convert to Number and save
276
+ story.zoom = Number(storyData.attributes.zoom);
277
+ }
278
+
279
+ /**
280
+ * options: (string) Optional.
281
+ * String of comma-separated key-value pairs for story.
282
+ * Each pair is separated by a comma.
283
+ */
284
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'options')) {
285
+ // Update metadata with options
286
+ const options = storyData.attributes.options;
287
+
288
+ // Split by comma and parse each key-value pair
289
+ if (options.length > 0) {
290
+ const pairs = options.split(',');
291
+ pairs.forEach(pair => {
292
+ const [key, value] = pair.split(':');
293
+ if (key && value) {
294
+ story.metadata[key.trim()] = value.trim();
295
+ }
296
+ });
297
+ }
298
+ }
299
+
300
+ /**
301
+ * hidden: (string) Optional.
302
+ * String of passage names that should not be included in the output.
303
+ */
304
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'hidden')) {
305
+ // Update metadata with hidden passages
306
+ story.metadata.hidden = storyData.attributes.hidden;
307
+ }
308
+
309
+ // Parse tag colors from style elements
310
+ const styleElements = dom.getElementsByTagName('style');
311
+ styleElements.forEach(styleElement => {
312
+ const styleContent = styleElement.innerHTML || styleElement.rawText || '';
313
+
314
+ // Look for tag color definitions
315
+ const tagColorRegex = /tw-story-tag-(.+?)\s*{\s*color:\s*(.+?)\s*}/g;
316
+ let match;
317
+
318
+ while ((match = tagColorRegex.exec(styleContent)) !== null) {
319
+ const tagName = match[1];
320
+ const color = match[2];
321
+ story.tagColors[tagName] = color;
322
+ }
323
+ });
324
+
325
+ // Pull out the `<tw-passagedata>` elements.
326
+ const storyPassages = dom.getElementsByTagName('tw-passagedata');
327
+
328
+ // Move through the passages.
329
+ storyPassages.forEach(passage => {
330
+ // Get the passage attributes.
331
+ const attr = passage.attributes;
332
+ // Get the passage text and decode HTML entities.
333
+ const text = decode(passage.rawText);
334
+
335
+ /**
336
+ * name: (string) Required.
337
+ * The name of the passage.
338
+ */
339
+ if (!Object.prototype.hasOwnProperty.call(attr, 'name')) {
340
+ // Name is required! Warn user and skip passage.
341
+ console.warn('Warning: Cannot parse passage data without name!');
342
+ return;
343
+ }
344
+
345
+ // Get passage name.
346
+ const name = attr.name;
347
+
348
+ /**
349
+ * pid: (string) Required.
350
+ * The Passage ID (PID).
351
+ */
352
+ if (!Object.prototype.hasOwnProperty.call(attr, 'pid')) {
353
+ // PID is required! Throw error.
354
+ throw new Error('Error: Passages are required to have PID!');
355
+ }
356
+
357
+ /**
358
+ * position: (string) Optional.
359
+ * Comma-separated X and Y coordinates of the passage within Twine 2.
360
+ */
361
+ let position = null;
362
+ if (Object.prototype.hasOwnProperty.call(attr, 'position')) {
363
+ position = attr.position;
364
+ }
365
+
366
+ /**
367
+ * size: (string) Optional.
368
+ * Comma-separated width and height of the passage within Twine 2.
369
+ */
370
+ let size = null;
371
+ if (Object.prototype.hasOwnProperty.call(attr, 'size')) {
372
+ size = attr.size;
373
+ }
374
+
375
+ /**
376
+ * tags: (string) Optional.
377
+ * Space-separated list of passage tags, if any.
378
+ */
379
+ let tags = [];
380
+ if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
381
+ if (attr.tags.length > 0 && attr.tags !== '""') {
382
+ tags = attr.tags.split(' ').filter(tag => tag !== '');
383
+ }
384
+ }
385
+
386
+ /**
387
+ * metadata: (object) Optional.
388
+ * An object containing additional metadata about the passage.
389
+ */
390
+ const metadata = {};
391
+
392
+ // Does position exist?
393
+ if (position !== null) {
394
+ metadata.position = position;
395
+ }
396
+
397
+ // Does size exist?
398
+ if (size !== null) {
399
+ metadata.size = size;
400
+ }
401
+
402
+ /**
403
+ * pid: (string) Required.
404
+ * The Passage ID (PID).
405
+ */
406
+ const pid = attr.pid;
407
+
408
+ // If the PID is the start node, update the story start.
409
+ if (startNode === pid) {
410
+ story.start = name;
411
+ }
412
+
413
+ // Add the passage.
414
+ story.addPassage(
415
+ new Passage(
416
+ name,
417
+ text,
418
+ tags,
419
+ metadata
420
+ )
421
+ );
422
+ });
423
+
424
+ // Return story object.
425
+ return story;
426
+ }
427
+
428
+ export { parse };
@@ -0,0 +1,31 @@
1
+ // Core web build - Common parsers and functionality
2
+ import { parse as parseTwee } from '../Twee/parse.js';
3
+ import { parse as parseJSON } from '../JSON/parse.js';
4
+ import { parse as parseStoryFormat } from '../StoryFormat/parse.js';
5
+ import { parse as parseTwine2HTML } from '../Twine2HTML/parse-web.js';
6
+ import { compile as compileTwine2HTML } from '../Twine2HTML/compile.js';
7
+ import { generate as generateIFID } from '../IFID/generate.js';
8
+ import { Story } from '../Story.js';
9
+ import Passage from '../Passage.js';
10
+ import StoryFormat from '../StoryFormat.js';
11
+
12
+ // Core functionality - most commonly used
13
+ window.Extwee = {
14
+ // Core parsers (immediately available)
15
+ parseTwee,
16
+ parseJSON,
17
+ parseStoryFormat,
18
+ parseTwine2HTML,
19
+
20
+ // Core compiler
21
+ compileTwine2HTML,
22
+
23
+ // Core utilities
24
+ generateIFID,
25
+ Story,
26
+ Passage,
27
+ StoryFormat,
28
+
29
+ // Version info
30
+ version: '2.3.2'
31
+ };
@@ -0,0 +1,31 @@
1
+ import { parse as parseTwee } from '../Twee/parse.js';
2
+ import { parse as parseJSON } from '../JSON/parse.js';
3
+ import { parse as parseStoryFormat } from '../StoryFormat/parse.js';
4
+ import { parse as parseTwine1HTML } from '../Twine1HTML/parse.js';
5
+ import { parse as parseTwine2HTML } from '../Twine2HTML/parse.js';
6
+ import { parse as parseTwine2ArchiveHTML } from '../Twine2ArchiveHTML/parse.js';
7
+ import { parse as parseTWS } from '../TWS/parse.js';
8
+ import { compile as compileTwine1HTML } from '../Twine1HTML/compile.js';
9
+ import { compile as compileTwine2HTML } from '../Twine2HTML/compile.js';
10
+ import { compile as compileTwine2ArchiveHTML } from '../Twine2ArchiveHTML/compile.js';
11
+ import { generate as generateIFID } from '../IFID/generate.js';
12
+ import { Story } from '../Story.js';
13
+ import Passage from '../Passage.js';
14
+ import StoryFormat from '../StoryFormat.js';
15
+
16
+ window.Extwee = {
17
+ parseTwee,
18
+ parseJSON,
19
+ parseTWS,
20
+ parseStoryFormat,
21
+ parseTwine1HTML,
22
+ parseTwine2HTML,
23
+ parseTwine2ArchiveHTML,
24
+ compileTwine1HTML,
25
+ compileTwine2HTML,
26
+ compileTwine2ArchiveHTML,
27
+ generateIFID,
28
+ Story,
29
+ Passage,
30
+ StoryFormat
31
+ };
@@ -0,0 +1,15 @@
1
+ // Twine1HTML parser module
2
+ import { parse as parseTwine1HTML } from '../Twine1HTML/parse-web.js';
3
+ import { compile as compileTwine1HTML } from '../Twine1HTML/compile.js';
4
+
5
+ // Export for use as a separate module
6
+ export {
7
+ parseTwine1HTML as parse,
8
+ compileTwine1HTML as compile
9
+ };
10
+
11
+ // Also add to global Extwee if it exists
12
+ if (typeof window !== 'undefined' && window.Extwee) {
13
+ window.Extwee.parseTwine1HTML = parseTwine1HTML;
14
+ window.Extwee.compileTwine1HTML = compileTwine1HTML;
15
+ }
@@ -0,0 +1,15 @@
1
+ // Twine2ArchiveHTML parser module
2
+ import { parse as parseTwine2ArchiveHTML } from '../Twine2ArchiveHTML/parse-web.js';
3
+ import { compile as compileTwine2ArchiveHTML } from '../Twine2ArchiveHTML/compile.js';
4
+
5
+ // Export for use as a separate module
6
+ export {
7
+ parseTwine2ArchiveHTML as parse,
8
+ compileTwine2ArchiveHTML as compile
9
+ };
10
+
11
+ // Also add to global Extwee if it exists
12
+ if (typeof window !== 'undefined' && window.Extwee) {
13
+ window.Extwee.parseTwine2ArchiveHTML = parseTwine2ArchiveHTML;
14
+ window.Extwee.compileTwine2ArchiveHTML = compileTwine2ArchiveHTML;
15
+ }
@@ -0,0 +1,12 @@
1
+ // TWS parser module
2
+ import { parse as parseTWS } from '../TWS/parse.js';
3
+
4
+ // Export for use as a separate module
5
+ export {
6
+ parseTWS as parse
7
+ };
8
+
9
+ // Also add to global Extwee if it exists
10
+ if (typeof window !== 'undefined' && window.Extwee) {
11
+ window.Extwee.parseTWS = parseTWS;
12
+ }
@@ -1,4 +1,4 @@
1
- import {reader as ConfigReader} from '../../src/Config/reader.js';
1
+ import { reader as ConfigReader } from '../../src/Config/reader.js';
2
2
  import {parser as ConfigParser} from '../../src/Config/parser.js';
3
3
 
4
4
  describe('src/Config/reader.js', () => {
@@ -1,7 +1,13 @@
1
- import { isDirectory } from '../../src/CLI/isDirectory';
2
- import { statSync } from 'node:fs';
1
+ import {jest} from '@jest/globals';
3
2
 
4
- jest.mock('node:fs');
3
+ // Mock the fs module before importing anything that uses it
4
+ const mockStatSync = jest.fn();
5
+ jest.unstable_mockModule('node:fs', () => ({
6
+ statSync: mockStatSync
7
+ }));
8
+
9
+ // Now import the modules that depend on the mocked module
10
+ const { isDirectory } = await import('../../src/CLI/isDirectory.js');
5
11
 
6
12
  describe('isDirectory', () => {
7
13
  afterEach(() => {
@@ -10,32 +16,32 @@ describe('isDirectory', () => {
10
16
 
11
17
  it('should return true if the path is a directory', () => {
12
18
  const mockStats = { isDirectory: jest.fn(() => true) };
13
- statSync.mockReturnValue(mockStats);
19
+ mockStatSync.mockReturnValue(mockStats);
14
20
 
15
21
  const result = isDirectory('/valid/directory/path');
16
- expect(statSync).toHaveBeenCalledWith('/valid/directory/path');
22
+ expect(mockStatSync).toHaveBeenCalledWith('/valid/directory/path');
17
23
  expect(mockStats.isDirectory).toHaveBeenCalled();
18
24
  expect(result).toBe(true);
19
25
  });
20
26
 
21
27
  it('should return false if the path is not a directory', () => {
22
28
  const mockStats = { isDirectory: jest.fn(() => false) };
23
- statSync.mockReturnValue(mockStats);
29
+ mockStatSync.mockReturnValue(mockStats);
24
30
 
25
31
  const result = isDirectory('/valid/file/path');
26
- expect(statSync).toHaveBeenCalledWith('/valid/file/path');
32
+ expect(mockStatSync).toHaveBeenCalledWith('/valid/file/path');
27
33
  expect(mockStats.isDirectory).toHaveBeenCalled();
28
34
  expect(result).toBe(false);
29
35
  });
30
36
 
31
37
  it('should return false and log an error if statSync throws an error', () => {
32
38
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
33
- statSync.mockImplementation(() => {
39
+ mockStatSync.mockImplementation(() => {
34
40
  throw new Error('Test error');
35
41
  });
36
42
 
37
43
  const result = isDirectory('/invalid/path');
38
- expect(statSync).toHaveBeenCalledWith('/invalid/path');
44
+ expect(mockStatSync).toHaveBeenCalledWith('/invalid/path');
39
45
  expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Test error'));
40
46
  expect(result).toBe(false);
41
47
 
@@ -1,11 +1,14 @@
1
- import { isFile } from '../../src/CLI/isFile.js';
2
- import { statSync } from 'node:fs';
1
+ import {jest} from '@jest/globals';
3
2
 
4
- // Mock the statSync function from 'fs'.
5
- jest.mock('node:fs', () => ({
6
- statSync: jest.fn(),
3
+ // Mock the fs module before importing anything that uses it
4
+ const mockStatSync = jest.fn();
5
+ jest.unstable_mockModule('node:fs', () => ({
6
+ statSync: mockStatSync
7
7
  }));
8
8
 
9
+ // Now import the modules that depend on the mocked module
10
+ const { isFile } = await import('../../src/CLI/isFile.js');
11
+
9
12
  describe('isFile', () => {
10
13
  afterEach(() => {
11
14
  jest.clearAllMocks();
@@ -13,36 +16,36 @@ describe('isFile', () => {
13
16
 
14
17
  it('should return true if the path is a valid file', () => {
15
18
  // Mock statSync to return an object with isFile() returning true.
16
- statSync.mockReturnValue({
19
+ mockStatSync.mockReturnValue({
17
20
  isFile: jest.fn(() => true),
18
21
  });
19
22
 
20
23
  const result = isFile('/path/to/file');
21
24
  expect(result).toBe(true);
22
- expect(statSync).toHaveBeenCalledWith('/path/to/file');
25
+ expect(mockStatSync).toHaveBeenCalledWith('/path/to/file');
23
26
  });
24
27
 
25
28
  it('should return false if the path is not a valid file', () => {
26
29
  // Mock statSync to return an object with isFile() returning false.
27
- statSync.mockReturnValue({
30
+ mockStatSync.mockReturnValue({
28
31
  isFile: jest.fn(() => false),
29
32
  });
30
33
 
31
34
  const result = isFile('/path/to/directory');
32
35
  expect(result).toBe(false);
33
- expect(statSync).toHaveBeenCalledWith('/path/to/directory');
36
+ expect(mockStatSync).toHaveBeenCalledWith('/path/to/directory');
34
37
  });
35
38
 
36
39
  it('should return false and log an error if statSync throws an error', () => {
37
40
  // Mock statSync to throw an error.
38
41
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
39
- statSync.mockImplementation(() => {
42
+ mockStatSync.mockImplementation(() => {
40
43
  throw new Error('File not found');
41
44
  });
42
45
 
43
46
  const result = isFile('/invalid/path');
44
47
  expect(result).toBe(false);
45
- expect(statSync).toHaveBeenCalledWith('/invalid/path');
48
+ expect(mockStatSync).toHaveBeenCalledWith('/invalid/path');
46
49
  expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Error: File not found'));
47
50
 
48
51
  consoleErrorSpy.mockRestore();