-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathBundler.js
More file actions
231 lines (185 loc) · 6.76 KB
/
Bundler.js
File metadata and controls
231 lines (185 loc) · 6.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-purple; icon-glyph: file-archive;
const { Files } = importModule("Files");
const { JS_EXTENSION, EMPTY_STRING } = importModule("Constants");
/**
* Represents metadata for a specific file, including its name and location.
* Provides utility methods to resolve full system paths.
*/
class FileInfo {
/** @type {string} @private */
#name;
/** @type {string} @private */
#directory;
/** @type {IterableIterator<string>|Array<string>} @private */
#dependencies;
/**
* Creates an instance of FileInfo.
* @param {string} name - The name of the file.
* @param {string} directory - The directory path.
* @param {IterableIterator<string>} dependencies - List of dependency names.
*/
constructor(name, directory, dependencies) {
this.#name = name;
this.#directory = directory;
this.#dependencies = dependencies;
}
/** @returns {string} The filename. */
name() {
return this.#name;
}
/** @returns {string} The directory path. */
directory() {
return this.#directory;
}
/** @returns {string} The full file path. */
path() {
return Files.joinPaths(this.directory(), this.name());
}
/** @returns {IterableIterator<string>} The list of script dependencies. */
dependencies() {
return this.#dependencies;
}
}
/**
* Used to compose script and all of its
* dependencies into single file and then
* store it in Scriptable root directory.
*
* @class Bundler
*/
class Bundler {
static #DEPENDENCY_REGEXP = new RegExp(/const\s*\{[^}]+\}\s*=\s*importModule\(["']([^"']+)["']\);?/g);
static #MODULE_EXPORTS_REGEXP = new RegExp(/module\.exports\s*=\s*{[^}]+};?/g);
#scriptName;
#scriptsDirectory;
#scriptMetadata = new Map();
#dependencyScripts = new Map();
/**
* Creates an instance of Bundler.
* @param {String} scriptName script that should be bundled
* @memberof Bundler
*/
constructor(scriptName, scriptsDirectory) {
this.#scriptName = scriptName;
this.#scriptsDirectory = scriptsDirectory;
}
/**
* Composes script and all of its
* dependencies into single file and then
* store it in Scriptable root directory.
*
* @memberof Bundler
*/
async bundle() {
this.#processDependencies(this.#scriptName);
let scriptBody = "";
const mainScriptMetadata = this.#scriptMetadata.get(this.#scriptName);
const mainScriptBody = this.#dependencyScripts.get(this.#scriptName);
// Delete main script from dependencies.
this.#dependencyScripts.delete(this.#scriptName);
// Add back removed metadata.
scriptBody += mainScriptMetadata;
// Add all dependencies.
for (const dependencyBody of this.#dependencyScripts.values()) {
scriptBody += dependencyBody;
}
// Add main script.
scriptBody += mainScriptBody;
const targetFileName = `${this.#scriptName} (Bundled)${JS_EXTENSION}`;
const targetFilePath = Files.joinPaths(this.#scriptsDirectory, targetFileName);
await Files.updateScriptableFile(targetFilePath, scriptBody);
return new FileInfo(
targetFileName,
this.#scriptsDirectory,
[...this.#dependencyScripts.keys()]
);
}
/**
* Used to process provided script by removing
* and then storing its metadata, dependency scripts and
* module exports blocks.
*
* Script is recursive and will process all dependencies as well.
*
* @param {String} scriptName script that should be processed
* @memberof Bundler
*/
#processDependencies(scriptName) {
const scriptPath = Files.joinPaths(this.#scriptsDirectory, scriptName + JS_EXTENSION);
let scriptBody = Files.readScriptableFile(scriptPath);
scriptBody = this.#extractMetadataAndGet(scriptName, scriptBody);
const scriptDependencies = this.#getScriptDependencies(scriptBody);
scriptBody = this.#removeDependenciesAndGet(scriptBody);
for (const dependencyName of scriptDependencies) {
// Don't process if it was already processed
// in previous scripts.
if (this.#dependencyScripts.has(dependencyName)) {
continue;
}
this.#processDependencies(dependencyName);
}
this.#dependencyScripts.set(scriptName, scriptBody);
}
/**
* Used to get list of dependency scripts
* by analyzing provided content.
*
* @param {String} scriptBody script content
* @return {List<String>} list of dependency scripts
* @memberof Bundler
*/
#getScriptDependencies(scriptBody) {
const dependencyMatches = [...scriptBody.matchAll(Bundler.#DEPENDENCY_REGEXP)];
return dependencyMatches.map(match => match[1]);
}
/**
* Used to update provided script by
* removing all dependency and module exports
* blocks.
*
* @param {String} scriptBody script content
* @return {String} updated script content
* @memberof Bundler
*/
#removeDependenciesAndGet(scriptBody) {
const dependencyMatches = [...scriptBody.matchAll(Bundler.#DEPENDENCY_REGEXP)];
for (const match of dependencyMatches) {
const importModuleBlock = match[0];
scriptBody = scriptBody.replaceAll(importModuleBlock, EMPTY_STRING);
}
const exportMatches = [...scriptBody.matchAll(Bundler.#MODULE_EXPORTS_REGEXP)];
for (const match of exportMatches) {
const exportBlock = match[0];
scriptBody = scriptBody.replaceAll(exportBlock, EMPTY_STRING);
}
return scriptBody;
}
/**
* Used to remove Scriptable metadata from
* provided script.
*
* Removed metadata is being stored for later use.
*
* @param {String} scriptName script name
* @param {String} scriptBody script content
* @return {String} updated script content without metadata
* @memberof Bundler
*/
#extractMetadataAndGet(scriptName, scriptBody) {
const scriptBodyLines = scriptBody.split('\n');
const metadataLines = scriptBodyLines.splice(0, 3);
// Store metadata to later use it when building
// back composed script.
this.#scriptMetadata.set(scriptName, metadataLines.join('\n'));
return scriptBodyLines.join('\n');
}
}
async function bundleScript(scriptName, scriptDirectory) {
const bundler = new Bundler(scriptName, scriptDirectory);
return bundler.bundle();
}
module.exports = {
bundleScript
};