This repository has been archived by the owner on Jul 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
rollup-plugin.js
218 lines (205 loc) · 11.6 KB
/
rollup-plugin.js
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
const fs = require("fs");
const path = require("path");
const rollup = require("rollup");
const commonjs = require("@rollup/plugin-commonjs");
const nodeResolve = require("@rollup/plugin-node-resolve").nodeResolve;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
/**
* The WebScience Rollup plugin. This plugin is necessary because certain WebScience assets (e.g., content
* scripts and HTML) are not handled by ordinary Rollup bundling. Developers building browser extensions
* with WebScience should, for all anticipated use cases, be able to use the Rollup plugin as-is.
*
* The plugin involves the following steps.
* * Identify WebScience asset dependencies, which are import statements that begin with an "include:" schema.
* * Resolve each WebScience asset dependency to a string containing an output path relative to the extension
* base directory. This format is required by WebExtensions APIs.
* * Allow Rollup tree shaking to occur, so that only necessary asset dependencies are bundled.
* * If an asset dependency is a content script (ending in .content.js), use Rollup to bundle the content script
* to the output directory in IIFE format, with support for Node module resolution and CommonJS module wrapping.
* This step allows WebScience content scripts to use components of the library and npm dependencies, and it
* makes debugging content script behavior straightforward.
* * If an asset dependency is an HTML file (ending in .html), copy the file to the output directory, parse the file,
* identify script and stylesheet dependencies (`<script src="..."></script>` and `<link rel="stylesheet" href="...">`),
* and copy those additional dependencies to the output directory. The plugin currently only supports additional HTML
* dependencies that are in the same source directory as the HTML file.
* * If an asset dependency is not one of the above types, copy the file to the output directory.
* @param {Object} [options] - Options for the plugin.
* @param {string} [options.manifestPath="./manifest.json"] - The path to the WebExtensions manifest.json, either
* absolute or relative to the current working directory. The plugin requires this path so that it can generate
* paths relative to the extension base directory for WebExtensions APIs.
* @param {string} [options.outputDirectory="./dist/webScience/"] - The directory where the plugin should output
* required dependencies, either absolute or relative to the current working directory. This directory must
* be equal to or a subdirectory of the directory containing the WebExtensions manifest.json.
* @returns {Object} - The generated Rollup plugin.
*/
module.exports = function webScienceRollupPlugin({
manifestPath = "./manifest.json",
outputDirectory = "./dist/webScience/"
} = {
manifestPath: "./manifest.json",
outputDirectory: "./dist/webScience/"
}) {
const includeScheme = "include:";
// If the manifest path or output directory is relative, convert it to absolute with the current working directory
manifestPath = path.resolve(process.cwd(), manifestPath);
const manifestDirectory = path.dirname(manifestPath);
outputDirectory = path.resolve(process.cwd(), outputDirectory) + path.sep;
// Check that the output directory is either the manifest directory or a subdirectory of the manifest directory
if(path.relative(manifestDirectory, outputDirectory).startsWith("..")) {
throw new Error("Error: the Webscience Rollup plugin requires that the output directory be either the same as the extension manifest directory or a subdirectory of that directory.");
}
// Check that the WebExtensions manifest path is correct, since we need to generate paths relative to the manifest
if(!fs.existsSync(manifestPath)) {
throw new Error(`Error: the Webscience Rollup plugin requires either running Rollup in the base directory for the extension or specifying an extension manifest path.`);
}
// Check that the output directory is a directory path
if(!outputDirectory.endsWith(path.sep)) {
throw new Error("Error: the Webscience Rollup plugin requires a valid directory path.")
}
// If the output directory doesn't already exist, create it
if(!fs.existsSync(outputDirectory)) {
fs.mkdirSync(outputDirectory, {
recursive: true
});
}
/**
* Generate the absolute file input path, absolute file output path, and output path relative to the WebExtensions manifest
* directory for an import ID that uses the include scheme.
*/
const pathsFromId = id => {
// Remove the scheme to obtain the absolute file input path
const inputPath = id.substring(includeScheme.length);
// Add the file name to the output directory to obtain the absolute file output path
const outputPath = outputDirectory + path.basename(inputPath);
// Generate the output path relative to the WebExtensions manifest directory, since that's what WebExtensions APIs require
const relativeOutputPath = path.relative(manifestDirectory, outputPath);
return { inputPath, outputPath, relativeOutputPath };
};
const plugin = {
name: "webscience-rollup-plugin",
// When the Rollup build starts, check that plugin dependencies are present
buildStart({ plugins }) {
const pluginDependencies = {
"commonjs": "@rollup/plugin-commonjs",
"node-resolve": "@rollup/plugin-node-resolve"
};
const pluginNames = new Set();
for(const plugin of plugins) {
pluginNames.add(plugin.name);
}
for(const pluginName in pluginDependencies) {
if(!pluginNames.has(pluginName)) {
throw new Error(`Error: bundling with the WebScience library requires ${pluginDependencies[pluginName]}.`);
}
}
},
async resolveId(source, importer) {
// Ignore bundling entry points
if(!importer) {
return null;
}
// Ignore import statements that don't start with the include scheme
if(!source.startsWith(includeScheme)) {
return null;
}
// Remove the scheme, resolve the absolute path for the import, then restore the scheme
source = source.substring(includeScheme.length);
const resolution = await this.resolve(source, importer, { skipSelf: true });
if(resolution === null || !("id" in resolution)) {
throw new Error(`Error: unable to resolve WebScience dependency: ${source}.`);
}
return includeScheme + resolution.id;
},
async load(id) {
// Ignore resolved import statements that don't start with the include scheme
if(!id.startsWith(includeScheme)) {
return null;
}
// Return a string that is the output path relative to the WebExtensions manifest directory, since that's what WebExtensions APIs require
return `export default "${pathsFromId(id).relativeOutputPath}";`;
},
// Generate output files with the generateBundle hook, rather than the load hook, so we can benefit from tree shaking
async generateBundle(options, bundle) {
// Identify all the import IDs with the include scheme
const idsWithIncludeScheme = new Set();
for(const info of Object.values(bundle)) {
if(info.type === "chunk") {
for(const id in info.modules) {
if(id.startsWith(includeScheme)) {
idsWithIncludeScheme.add(id);
}
}
}
}
// Generate output files for each import ID with the include scheme
for(const id of idsWithIncludeScheme) {
const { inputPath, outputPath } = pathsFromId(id);
// If the file is a content script (i.e., ends with .content.js), bundle it to the output directory in IIFE format
if(inputPath.endsWith(".content.js")) {
const outputBundle = await rollup.rollup({
input: inputPath,
plugins: [
// If we encounter an import with the include scheme when bundling, just return an empty string and let Rollup tree shake the import
{
name: "ignore-include-scheme",
resolveId: plugin.resolveId,
load(id) {
if(id.startsWith(includeScheme)) {
return `export default "";`;
}
}
},
commonjs(),
nodeResolve({
browser: true,
moduleDirectories: [
process.cwd() + path.sep + "node_modules"
]
})
]
});
await outputBundle.write({
output: {
file: outputPath,
format: "iife"
}
});
await outputBundle.close();
}
// If the file is HTML (i.e., ends with .html), copy the file and any script or stylesheet dependencies
else if(inputPath.endsWith("html")) {
// Copy the HTML file
fs.copyFileSync(inputPath, outputPath);
// Parse the HTML file and extract script and stylesheet dependency paths
const html = fs.readFileSync(inputPath, "utf8");
const dom = new JSDOM(html);
const embeddedFileRelativePaths = new Set();
const scriptElements = dom.window.document.querySelectorAll("script[src]");
for(const scriptElement of scriptElements) {
embeddedFileRelativePaths.add(scriptElement.src);
}
const stylesheetElements = dom.window.document.querySelectorAll("link[rel=stylesheet][href]");
for(const stylesheetElement of stylesheetElements) {
embeddedFileRelativePaths.add(stylesheetElement.href);
}
// Generate output paths for dependencies and copy the files
for(const embeddedFileRelativePath of embeddedFileRelativePaths) {
const embeddedFileAbsolutePath = path.resolve(path.dirname(inputPath), embeddedFileRelativePath);
if(path.dirname(embeddedFileAbsolutePath) === path.dirname(inputPath)) {
fs.copyFileSync(embeddedFileAbsolutePath, outputDirectory + path.basename(embeddedFileAbsolutePath));
}
else {
console.warn(`Warning: the Webscience Rollup plugin only supports HTML script and stylesheet embeds in the same directory as the HTML file. Unable to embed ${embeddedFileAbsolutePath} in ${outputPath}.`);
}
}
}
// For all other file types, copy the file to the output directory
else {
fs.copyFileSync(inputPath, outputPath);
}
}
}
};
return plugin;
};