extwee 2.2.5 → 2.3.0

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 (63) hide show
  1. package/.github/workflows/dependabot-automerge.yml +23 -0
  2. package/.github/workflows/nodejs.yml +4 -1
  3. package/README.md +29 -14
  4. package/SECURITY.md +1 -1
  5. package/build/extwee.web.min.js +2 -0
  6. package/build/extwee.web.min.js.LICENSE.txt +1 -0
  7. package/extwee.config.json +6 -0
  8. package/extwee.config.md +67 -0
  9. package/index.js +2 -0
  10. package/package.json +24 -23
  11. package/src/CLI/CommandLineProcessing.js +196 -0
  12. package/src/CLI/ProcessConfig/loadStoryFormat.js +102 -0
  13. package/src/CLI/ProcessConfig/readDirectories.js +46 -0
  14. package/src/CLI/ProcessConfig.js +175 -0
  15. package/src/CLI/isDirectory.js +27 -0
  16. package/src/CLI/isFile.js +28 -0
  17. package/src/Config/parser.js +30 -8
  18. package/src/Passage.js +17 -2
  19. package/src/Story.js +101 -1
  20. package/src/StoryFormat/compile.js +19 -0
  21. package/src/StoryFormat.js +51 -0
  22. package/src/extwee.js +20 -195
  23. package/test/Config/Config.test.js +40 -10
  24. package/test/Config/files/full.json +8 -0
  25. package/test/Config/files/valid.json +4 -3
  26. package/test/Config/isDirectory.test.js +44 -0
  27. package/test/Config/isFile.test.js +50 -0
  28. package/test/Config/loadStoryFormat.test.js +101 -0
  29. package/test/Config/readDirectories.test.js +68 -0
  30. package/test/Objects/Passage.test.js +5 -0
  31. package/test/Objects/Story.test.js +174 -0
  32. package/test/Objects/StoryFormat.test.js +60 -0
  33. package/test/TWS/Parse.test.js +0 -22
  34. package/test/Web/window.Extwee.test.js +85 -0
  35. package/types/Story.d.ts +26 -1
  36. package/types/StoryFormat/compile.d.ts +8 -0
  37. package/types/StoryFormat.d.ts +7 -0
  38. package/types/index.d.ts +4 -2
  39. package/types/src/CLI/CommandLineProcessing.d.ts +8 -0
  40. package/types/src/CLI/ProcessConfig/loadStoryFormat.d.ts +20 -0
  41. package/types/src/CLI/ProcessConfig/readDirectories.d.ts +9 -0
  42. package/types/src/CLI/ProcessConfig.d.ts +12 -0
  43. package/types/src/CLI/isDirectory.d.ts +1 -0
  44. package/types/src/CLI/isFile.d.ts +1 -0
  45. package/types/src/Config/parser.d.ts +6 -0
  46. package/types/src/Config/reader.d.ts +11 -0
  47. package/types/src/IFID/generate.d.ts +14 -0
  48. package/types/src/JSON/parse.d.ts +44 -1
  49. package/types/src/Passage.d.ts +49 -4
  50. package/types/src/Story.d.ts +110 -16
  51. package/types/src/StoryFormat/compile.d.ts +8 -0
  52. package/types/src/StoryFormat/parse.d.ts +46 -3
  53. package/types/src/StoryFormat.d.ts +69 -38
  54. package/types/src/TWS/parse.d.ts +3 -3
  55. package/types/src/Twee/parse.d.ts +3 -4
  56. package/types/src/Twine1HTML/compile.d.ts +3 -1
  57. package/types/src/Twine1HTML/parse.d.ts +3 -4
  58. package/types/src/Twine2ArchiveHTML/compile.d.ts +8 -0
  59. package/types/src/Twine2ArchiveHTML/parse.d.ts +31 -1
  60. package/types/src/Twine2HTML/compile.d.ts +7 -2
  61. package/types/src/Twine2HTML/parse.d.ts +12 -9
  62. package/index.html +0 -22
  63. package/test/TWS/TWSParser/Example1.tws +0 -150
@@ -0,0 +1 @@
1
+ /*! https://mths.be/he v1.2.0 by @mathias | MIT license */
@@ -0,0 +1,6 @@
1
+ {
2
+ "mode": "decompile",
3
+ "input": "index.html",
4
+ "output": "index.twee",
5
+ "story-format": "harlowe"
6
+ }
@@ -0,0 +1,67 @@
1
+ # Extwee Config File Options
2
+
3
+ The configuration file supports some, but not all, possible actions using command-line arguments.
4
+
5
+ ## Defining `mode`
6
+
7
+ The most important option is `mode`. This **MUST BE** either `compile` or `decompile`.
8
+
9
+ ```json
10
+ {
11
+ "mode": "decompile"
12
+ }
13
+ ```
14
+
15
+ ## Defining `input` and `output`
16
+
17
+ To process files, the `input` and `output` properties **MUST BE** defined using either an absolute or relative path.
18
+
19
+ ```json
20
+ {
21
+ "mode": "compile",
22
+ "input": "index.twee",
23
+ "output": "index.html"
24
+ }
25
+ ```
26
+
27
+ ## Defining `story-format`
28
+
29
+ If using the `"mode": "compile"` option, the `story-format` property **MUST BE** defined. This should be the name of a directory in the relative `./story-formats` directory.
30
+
31
+ For example, if using Harlowe, the path would be `./story-formats/harlowe` and the key-value pair would be `"story-format": "harlowe"`.
32
+
33
+ ```json
34
+ {
35
+ "mode": "compile",
36
+ "input": "index.twee",
37
+ "output": "index.html",
38
+ "story-format": "harlowe"
39
+ }
40
+ ```
41
+
42
+ ### Defining optional `story-format-version`
43
+
44
+ The Story Format Archive retrieves story formats based on its version in a sub-directory structure:
45
+
46
+ ```file
47
+ story-formats/
48
+ ├── harlowe/
49
+ │ ├── 2.3.0/
50
+ │ └── format.js
51
+ │ ├── 2.4.0/
52
+ │ └── format.js
53
+ ```
54
+
55
+ This can be specified, such as `3.2.0`, or the default `latest`, can be used.
56
+
57
+ ```json
58
+ {
59
+ "mode": "compile",
60
+ "input": "index.twee",
61
+ "output": "index.html",
62
+ "story-format": "harlowe",
63
+ "story-format-version": "3.2.0"
64
+ }
65
+ ```
66
+
67
+ If only the story format name is specified, and it can be found in the local `story-formats` directory, it will search for a corresponding `format.js` file.
package/index.js CHANGED
@@ -8,6 +8,7 @@ import { parse as parseTWS } from './src/TWS/parse.js';
8
8
  import { compile as compileTwine1HTML } from './src/Twine1HTML/compile.js';
9
9
  import { compile as compileTwine2HTML } from './src/Twine2HTML/compile.js';
10
10
  import { compile as compileTwine2ArchiveHTML } from './src/Twine2ArchiveHTML/compile.js';
11
+ import { compile as compileStoryFormat } from './src/StoryFormat/compile.js';
11
12
  import { generate as generateIFID } from './src/IFID/generate.js';
12
13
  import { Story } from './src/Story.js';
13
14
  import Passage from './src/Passage.js';
@@ -24,6 +25,7 @@ export {
24
25
  compileTwine1HTML,
25
26
  compileTwine2HTML,
26
27
  compileTwine2ArchiveHTML,
28
+ compileStoryFormat,
27
29
  generateIFID,
28
30
  Story,
29
31
  Passage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.2.5",
3
+ "version": "2.3.0",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
@@ -12,9 +12,8 @@
12
12
  "lint": "eslint ./src/**/*.js --fix",
13
13
  "lint:test": "eslint ./test/**/*.test.js --fix",
14
14
  "build:web": "webpack",
15
- "build:bin": "esbuild ./src/extwee.js --bundle --platform=node --target=node12 --outfile=out.js",
16
15
  "gen-types": "npx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types",
17
- "all": "npm run lint && npm run lint:test && npm run test && npm run gen-types"
16
+ "all": "npm run lint && npm run lint:test && npm run test && npm run build:web && npm run gen-types"
18
17
  },
19
18
  "keywords": [
20
19
  "twine",
@@ -24,34 +23,36 @@
24
23
  ],
25
24
  "license": "MIT",
26
25
  "dependencies": {
27
- "commander": "^13.1.0",
26
+ "commander": "^14.0.0",
28
27
  "graphemer": "^1.4.0",
29
- "html-entities": "^2.5.2",
28
+ "html-entities": "^2.6.0",
30
29
  "node-html-parser": "^7.0.1",
31
30
  "pickleparser": "^0.2.1",
32
- "semver": "^7.7.1",
33
- "uuid": "^11.0.5"
31
+ "semver": "^7.7.2",
32
+ "shelljs": "^0.10.0",
33
+ "uuid": "^11.1.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@babel/cli": "^7.26.4",
37
- "@babel/core": "^7.26.8",
38
- "@babel/preset-env": "^7.26.8",
39
- "@eslint/js": "^9.20.0",
36
+ "@babel/cli": "^7.27.2",
37
+ "@babel/core": "^7.27.1",
38
+ "@babel/preset-env": "^7.27.2",
39
+ "@eslint/js": "^9.29.0",
40
+ "@inquirer/prompts": "^7.5.2",
41
+ "@types/semver": "^7.7.0",
40
42
  "@types/uuid": "^10.0.0",
41
- "babel-loader": "^9.2.1",
43
+ "babel-loader": "^10.0.0",
42
44
  "clean-jsdoc-theme": "^4.3.0",
43
- "core-js": "^3.40.0",
44
- "esbuild": "^0.25.0",
45
- "eslint": "^9.20.0",
46
- "eslint-plugin-jest": "^28.11.0",
47
- "eslint-plugin-jsdoc": "^50.6.3",
48
- "globals": "^15.14.0",
49
- "jest": "^29.7.0",
45
+ "core-js": "^3.43.0",
46
+ "eslint": "^9.29.0",
47
+ "eslint-plugin-jest": "^28.14.0",
48
+ "eslint-plugin-jsdoc": "^51.0.1",
49
+ "globals": "^16.2.0",
50
+ "jest": "^30.0.0",
51
+ "jest-environment-jsdom": "^30.0.0",
50
52
  "regenerator-runtime": "^0.14.1",
51
- "shelljs": "^0.8.5",
52
- "typescript": "^5.7.3",
53
- "typescript-eslint": "^8.23.0",
54
- "webpack": "^5.97.1",
53
+ "typescript": "^5.8.3",
54
+ "typescript-eslint": "^8.34.0",
55
+ "webpack": "^5.99.9",
55
56
  "webpack-cli": "^6.0.1"
56
57
  },
57
58
  "repository": {
@@ -0,0 +1,196 @@
1
+ // Import functions we need.
2
+ import {
3
+ parseTwine2HTML,
4
+ parseTwee,
5
+ parseStoryFormat,
6
+ parseTwine1HTML,
7
+ compileTwine2HTML,
8
+ compileTwine1HTML
9
+ } from '../../index.js';
10
+
11
+ // Import fs.
12
+ import { readFileSync, writeFileSync } from 'node:fs';
13
+
14
+ // Import Commander.
15
+ import { Command, InvalidArgumentError } from 'commander';
16
+
17
+ // Import package.json.
18
+ import Package from '../../package.json' with { type: 'json' };
19
+
20
+ // Import isFile function.
21
+ import { isFile } from './isFile.js';
22
+
23
+ /**
24
+ * Process command line arguments.
25
+ * @function CommandLineProcessing
26
+ * @description This function processes the command line arguments passed to the Extwee CLI.
27
+ * @module CLI/commandLineProcessing
28
+ * @param {Array} argv - The command line arguments passed to the CLI.
29
+ */
30
+ export function CommandLineProcessing(argv) {
31
+ // This is the main function for processing the command line arguments.
32
+ // It uses the Commander library to parse the arguments and then calls the appropriate functions.
33
+ // The function is called when the script is run from the command line.
34
+
35
+
36
+ // Create a new Command.
37
+ const program = new Command();
38
+
39
+ program
40
+ .name('extwee')
41
+ .description('CLI for Extwee')
42
+ .option('-v, --version', 'Show version number', () => {
43
+ // Show the version number.
44
+ console.log(`Extwee v${Package.version}`);
45
+ // Exit the process.
46
+ process.exit(0);
47
+ })
48
+ .option('-c, --compile', 'Compile input into output')
49
+ .option('-d, --decompile', 'De-compile input into output')
50
+ .option('--twine1', 'Enable Twine 1 processing')
51
+ .option('--name <storyFormatName>', 'Name of the Twine 1 story format (needed for `code.js` inclusion)')
52
+ .option('--codejs <codeJSFile>', 'Twine 1 code.js file for use with Twine 1 HTML', (value) => {
53
+ // Does the input file exist?
54
+ if (isFile(value) === false) {
55
+ // We cannot do anything without valid input.
56
+ throw new InvalidArgumentError(`Twine 1 code.js ${value} does not exist.`);
57
+ }
58
+
59
+ return value;
60
+ })
61
+ .option('--engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
62
+ // Does the input file exist?
63
+ if (isFile(value) === false) {
64
+ // We cannot do anything without valid input.
65
+ throw new InvalidArgumentError(`Twine 1 engine.js ${value} does not exist.`);
66
+ }
67
+
68
+ return value;
69
+ })
70
+ .option('--header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
71
+ // Does the input file exist?
72
+ if (isFile(value) === false) {
73
+ // We cannot do anything without valid input.
74
+ throw new InvalidArgumentError(`Twine 1 header.html ${value} does not exist.`);
75
+ }
76
+
77
+ return value;
78
+ })
79
+ .option('-s <storyformat>, --storyformat <storyformat>', 'Path to story format file for Twine 2', (value) => {
80
+ // Does the input file exist?
81
+ if (isFile(value) === false) {
82
+ // We cannot do anything without valid input.
83
+ throw new InvalidArgumentError(`Story format ${value} does not exist.`);
84
+ }
85
+
86
+ return value;
87
+ })
88
+ .option('-i <inputFile>, --input <inputFile>', 'Path to input file', (value) => {
89
+ // Does the input file exist?
90
+ if (isFile(value) === false) {
91
+ // We cannot do anything without valid input.
92
+ throw new InvalidArgumentError(`Input file ${value} does not exist.`);
93
+ }
94
+
95
+ return value;
96
+ })
97
+ .option('-o <outputFile>, --output <outputFile>', 'Path to output file');
98
+
99
+ // Parse the passed arguments.
100
+ program.parse(argv);
101
+
102
+ // Create object of passed arguments parsed by Commander.
103
+ const options = program.opts();
104
+
105
+ /*
106
+ * Prepare some (soon to be) global variables.
107
+ */
108
+ // Check if Twine 1 is enabled.
109
+ const isTwine1Mode = (options.twine1 === true);
110
+
111
+ // Check if Twine 2 is enabled.
112
+ const isTwine2Mode = (isTwine1Mode === false);
113
+
114
+ // Check if de-compile mode is enabled.
115
+ const isDecompileMode = (options.decompile === true);
116
+
117
+ // Check if compile mode is enabled.
118
+ const isCompileMode = (options.compile === true);
119
+
120
+ // De-compile Twine 2 HTML into Twee 3 branch.
121
+ // If -d is passed, -i and -o are required.
122
+ if (isTwine2Mode === true && isDecompileMode === true) {
123
+ // Read the input HTML file.
124
+ const inputHTML = readFileSync(options.i, 'utf-8');
125
+
126
+ // Parse the input HTML file into Story object.
127
+ const storyObject = parseTwine2HTML(inputHTML);
128
+
129
+ // Write the output file from Story as Twee 3.
130
+ writeFileSync(options.o, storyObject.toTwee());
131
+
132
+ return;
133
+ }
134
+
135
+ // Compile Twee 3 into Twine 2 HTML branch.
136
+ // If -c is passed, -i, -o, and -s are required.
137
+ if (isTwine2Mode === true && isCompileMode === true) {
138
+ // Read the input file.
139
+ const inputTwee = readFileSync(options.i, 'utf-8');
140
+
141
+ // Parse the input file.
142
+ const story = parseTwee(inputTwee);
143
+
144
+ // Read the story format file.
145
+ const inputStoryFormat = readFileSync(options.s, 'utf-8');
146
+
147
+ // Parse the story format file.
148
+ const parsedStoryFormat = parseStoryFormat(inputStoryFormat);
149
+
150
+ // Compile the story.
151
+ const Twine2HTML = compileTwine2HTML(story, parsedStoryFormat);
152
+
153
+ // Write the output file.
154
+ writeFileSync(options.o, Twine2HTML);
155
+
156
+ // Exit the process.
157
+ return;
158
+ }
159
+
160
+ // Compile Twee 3 into Twine 1 HTML branch.
161
+ // Twine 1 compilation is complicated, so we have to check for all the required options.
162
+ // * options.engine (from Twine 1 itself)
163
+ // * options.header (from Twine 1 story format)
164
+ // * options.name (from Twine 1 story format)
165
+ // * options.codejs (from Twine 1 story format)
166
+ if (isTwine1Mode === true && isCompileMode === true) {
167
+ // Read the input file.
168
+ const inputTwee = readFileSync(options.i, 'utf-8');
169
+
170
+ // Parse the input file.
171
+ const story = parseTwee(inputTwee);
172
+
173
+ // Does the engine file exist?
174
+ const Twine1HTML = compileTwine1HTML(story, options.engine, options.header, options.name, options.codejs);
175
+
176
+ // Write the output file.
177
+ writeFileSync(options.o, Twine1HTML);
178
+
179
+ // Exit the process.
180
+ return;
181
+ }
182
+
183
+ // De-compile Twine 1 HTML into Twee 3 branch.
184
+ if (isTwine1Mode === true && isDecompileMode === true) {
185
+ // Read the input HTML file.
186
+ const inputHTML = readFileSync(options.i, 'utf-8');
187
+
188
+ // Parse the input HTML file into Story object.
189
+ const storyObject = parseTwine1HTML(inputHTML);
190
+
191
+ // Write the output file from Story as Twee 3.
192
+ writeFileSync(options.o, storyObject.toTwee());
193
+
194
+ return;
195
+ }
196
+ }
@@ -0,0 +1,102 @@
1
+ import { isDirectory } from "../isDirectory.js";
2
+ import { isFile } from "../isFile.js";
3
+ import { readDirectories } from "./readDirectories.js";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ /**
7
+ * Load the story format from the story-formats directory.
8
+ * @function loadStoryFormat
9
+ * @description This function loads the story format from the story-formats directory.
10
+ * It checks if the story-formats directory exists, if the named story format exists,
11
+ * if the version directory exists, and if the format.js file exists.
12
+ * If any of these checks fail, the function will exit the process with an error message.
13
+ * If all checks pass, the function will return the contents of the format.js file.
14
+ * @param {string} storyFormatName - The name of the story format.
15
+ * @param {string} storyFormatVersion - The version of the story format.
16
+ * @returns {string} - The contents of the format.js file.
17
+ * @throws {Error} - If the story-formats directory does not exist, if the named story format does not exist,
18
+ * if the version directory does not exist, or if the format.js file does not exist.
19
+ * @example
20
+ * // Load the story format from the story-formats directory.
21
+ * const storyFormat = loadStoryFormat('Harlowe', '3.2.0');
22
+ * console.log(storyFormat);
23
+ * // Output: The contents of the format.js file.
24
+ */
25
+ export function loadStoryFormat(storyFormatName, storyFormatVersion) {
26
+ // If the story-formats directory does not exist, throw error.
27
+ if (isDirectory('story-formats') === false) {
28
+ throw new Error(`Error: story-formats directory does not exist. Consider running 'npx sfa-get' to download the latest story formats.`);
29
+ }
30
+
31
+ // Does the named story format exist in the story-formats directory?
32
+ const isStoryFormatDir = isDirectory(`story-formats/${storyFormatName}`);
33
+
34
+ // If the story format directory does not exist, throw error.
35
+ if (isStoryFormatDir === false) {
36
+ throw new Error(`Error: story format ${storyFormatName} does not exist in the story-formats directory.`);
37
+ }
38
+
39
+ let filepath = `story-formats/${storyFormatName}/format.js`;
40
+
41
+ // If the story format version is 'latest', check if the format.js file exists.
42
+ // If the format.js file does not exist, check if there are version directories.
43
+ // If there are version directories, get the latest version and set the filepath to that version directory and format.js file.
44
+ // If the format.js file exists, return its contents.
45
+
46
+ // Check if storyFormatVersion is "latest"
47
+ if (storyFormatVersion === 'latest' && isFile(filepath) === false) {
48
+ // Read the directories in the story format directory.
49
+ // The directories are expected to be version directories.
50
+ let directories = readDirectories(`story-formats/${storyFormatName}`);
51
+
52
+ console.log("!!! directories", directories);
53
+
54
+ // Check if there are any version directories.
55
+ if (directories.length === 0) {
56
+ // If there are no version directories, throw error.
57
+ throw new Error(`Error: story format ${storyFormatName} does not have any version directories.`);
58
+ }
59
+
60
+ // Sort the directories in descending order.
61
+ directories.sort((a, b) => {
62
+ return b.localeCompare(a, undefined, { numeric: true });
63
+ });
64
+
65
+ // Get the latest version directory.
66
+ // The latest version is the last directory in the sorted list.
67
+ const latestVersion = directories[0];
68
+
69
+ // Set the filepath to the latest version directory.
70
+ filepath = `story-formats/${storyFormatName}/${latestVersion}/format.js`;
71
+
72
+ // Is there a 'format.js' file in the version directory?
73
+ let isFormatFile = isFile(filepath);
74
+
75
+ // If the format.js file does not exist, exit the process.
76
+ if (isFormatFile === false) {
77
+ throw new Error(`Error: story format ${storyFormatName} version ${storyFormatVersion} does not have a format.js file.`);
78
+ }
79
+ }
80
+
81
+ // If the story format version is not 'latest', check if version directories exists.
82
+ if(storyFormatVersion !== 'latest') {
83
+ // Does the named story format have a version directory?
84
+ const isVersionDir = isDirectory(`story-formats/${storyFormatName}/${storyFormatVersion}`);
85
+
86
+ // If the version directory does not exist, throw error.
87
+ if (isVersionDir === false) {
88
+ throw new Error(`Error: story format ${storyFormatName} version ${storyFormatVersion} does not exist in the story-formats directory.`);
89
+ }
90
+
91
+ // Set an initial path based on the version.
92
+ filepath = `story-formats/${storyFormatName}/${storyFormatVersion}/format.js`;
93
+
94
+ // If the format.js file does not exist, throw error.
95
+ if (isFile(filepath) === false) {
96
+ throw new Error(`Error: story format ${storyFormatName} version ${storyFormatVersion} does not have a format.js file.`);
97
+ }
98
+ }
99
+
100
+ // If the format.js file exists, read and return its contents.
101
+ return readFileSync(filepath, 'utf-8');
102
+ }
@@ -0,0 +1,46 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { isDirectory } from '../isDirectory.js';
3
+
4
+ /**
5
+ * Read the contents of a directory and returns all directories.
6
+ * @function readDirectories
7
+ * @description This function reads the contents of a directory and returns a list of directories.
8
+ * @param {string} directory - The path to the directory to read.
9
+ * @returns {Array<string>} - An array of directories in the directory.
10
+ * @throws {Error} - If the directory does not exist or if there is an error reading the directory.
11
+ */
12
+ export function readDirectories(directory) {
13
+
14
+ // Create default response.
15
+ let results = [];
16
+
17
+ // Check if the directory exists.
18
+ const isDir = isDirectory(directory);
19
+ // If the directory does not exist, return an empty array
20
+ // and log an error message.
21
+ if (isDir == false) {
22
+ console.error(`Error: Directory ${directory} does not exist.`);
23
+ }
24
+
25
+ // Read the directory and return the list of files.
26
+ try {
27
+ results = readdirSync(directory);
28
+ } catch (error) {
29
+ console.error(`Error reading directory ${directory}:`, error);
30
+ results = [];
31
+ }
32
+
33
+ // Check if results is an array.
34
+ // This should not happen, but for some reason it can.
35
+ if (!Array.isArray(results)) {
36
+ results = [];
37
+ }
38
+
39
+ // Filter the list to only include directories.
40
+ const directoriesOnly = results.filter((item) => {
41
+ return isDirectory(`${directory}/${item}`);
42
+ });
43
+
44
+ // Return the list of directories.
45
+ return directoriesOnly;
46
+ }