-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
274 lines (246 loc) · 8.86 KB
/
index.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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
/**
* @typedef {Object} RollupPluginConfig
* @property {string} [shortcode="rollup"]
* @property {import("rollup").RollupOptions | string} rollupOptions
* @property {function(string):Promise<string>} resolveName
* @property {function(string):Promise<string>} scriptGenerator
* @property {string} [importScriptsAbsoluteFrom=eleventyConfig.dir.output] Path to use for absolute imports in the generated script. If falsy, the script will use the eleventy output directory.
* @property {boolean} [useAbsoluteScriptPaths=false] If true, the script will use absolute paths for the generated script. If false, the script will use relative paths.
*/
/**
* @typedef {import('@11ty/eleventy/src/UserConfig')} EleventyConfig
* @typedef {import('@11ty/eleventy/src/Eleventy')} Eleventy
*/
// If a file is used in multiple bundles, chunking might fail
let filesAcrossAllBundles = new Map();
/**
* Create an instance for a Rollup Plugin.
* Be aware that the config is not allowed to be an array of bundles yet - sorry.
* @param {EleventyConfig} eleventyConfig Config to use
* @param {RollupPluginConfig} options
*/
module.exports = (eleventyConfig, options) => {
new EleventyPluginRollup(eleventyConfig, options);
};
class EleventyPluginRollup {
inputFiles = {};
rollupConfigPromise;
rollupConfig;
resolveName;
scriptGenerator;
/**
* Create a new instance of the rollup plugin
* @param {EleventyConfig} eleventyConfig
* @param {RollupPluginConfig} options Configuration for the plugin instance
*/
constructor(
eleventyConfig,
{
shortcode = 'rollup',
resolveName = this.defaultNamingFunction,
scriptGenerator = this.defaultScriptGenerator,
rollupOptions,
importScriptsAbsoluteFrom,
useAbsoluteScriptPaths,
}
) {
this.importScriptsAbsoluteFrom =
importScriptsAbsoluteFrom || eleventyConfig.dir.output;
this.useAbsoluteScriptPaths = useAbsoluteScriptPaths;
this.rollupConfigPromise = this.loadRollupConfig(
rollupOptions,
eleventyConfig
);
this.resolveName = resolveName;
this.scriptGenerator = scriptGenerator;
eleventyConfig.on('beforeBuild', () => this.beforeBuild());
eleventyConfig.on('afterBuild', () => this.afterBuild());
// We want to use "this" in the callback function, so we save the class instance beforehand
const thisRollupPlugin = this;
eleventyConfig.addAsyncShortcode(shortcode, function (...args) {
return thisRollupPlugin.rollupperShortcode(this, ...args);
});
}
/**
* Load the config including resolving file names to files
* @param {import("rollup").RollupOptions | string} potentialConfig
* @returns {Promise<import("rollup").RollupOptions>} Resolved config
*/
async loadRollupConfig(potentialConfig, eleventyConfig) {
let config;
if (typeof potentialConfig === 'string') {
// Load from file
const configModule = await import(
path.resolve(process.cwd(), potentialConfig)
);
const configOrConfigResolver = configModule.default;
if (typeof configOrConfigResolver === 'function') {
config = configOrConfigResolver({});
} else {
config = configOrConfigResolver;
}
} else {
config = potentialConfig;
}
this.rollupConfig = config;
if (this.rollupConfig.watch && this.rollupConfig.watch.include) {
let includes = [];
if (this.rollupConfig.watch.include[Symbol.iterator]) {
includes = this.rollupConfig.watch.include;
} else {
includes = [this.rollupConfig.watch.include];
}
for (const watchInclude of includes) {
eleventyConfig.addWatchTarget(watchInclude);
}
}
return config;
}
/**
* Resolve a file to a unique, cacheable filename
* @param {string} resolvedPath Original path of the file
* @returns {string} Unique name
*/
async defaultNamingFunction(resolvedPath) {
const fileHash = await new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
// Include file path in hash to handle moved files
hash.update(resolvedPath);
hash.update('---MAGIC ELEVENTY ROLLUP PLUGIN DEVIDER---');
const input = fs.createReadStream(resolvedPath);
input.on('error', reject);
input.on('data', (chunk) => hash.update(chunk));
input.on('close', () => resolve(hash.digest('hex')));
});
// keep original filename in output filename
const parsedPath = path.parse(resolvedPath);
return `${parsedPath.name}-${fileHash.substr(0, 6)}.js`;
}
/**
* Reset the current instance of the plugin.
* This is needed when the build does a hot/watch reload.
*/
beforeBuild() {
this.inputFiles = {};
filesAcrossAllBundles = new Map();
}
/**
*
* @param {Eleventy} eleventyInstance Currently executing 11ty instance
* @param {string} src Path to JS file
* @param {boolean} [isFileRelative=false] Should the file resolve relative to the current template?
* @returns
*/
async rollupperShortcode(eleventyInstance, src, isFileRelative = false) {
// Return early if page is not rendered to filesystem to avoid errors and remove unnecessary files from bundle.
if (eleventyInstance.page.outputPath === false) {
return;
}
await this.rollupConfigPromise;
// Resolve to the correct relative location
if (isFileRelative) {
src = path.resolve(path.dirname(eleventyInstance.page.inputPath), src);
}
// resolve to absolute, since rollup uses absolute paths
src = path.resolve(src);
src = path.relative('.', src);
if (
filesAcrossAllBundles.has(src) &&
filesAcrossAllBundles.get(src) !== this
) {
console.warn(
`eleventy-plugin-rollup warning: ${src} is used in multiple bundles, this might lead to unwanted sideeffects!`
);
}
filesAcrossAllBundles.set(src, this);
// resolveName is potentially very expensive, so avoid unnecessary executions of it
// -> this plugin assumes that every js file is stable during a build
if (!(src in this.inputFiles)) {
const scriptSrc = await this.resolveName(src);
// register for rollup bundling
this.inputFiles[src] = scriptSrc;
}
const relativeFrom = this.useAbsoluteScriptPaths
? this.importScriptsAbsoluteFrom
: path.dirname(eleventyInstance.page.outputPath);
// calculate script src after bundling
const importPath = path
.join(
this.useAbsoluteScriptPaths ? '/' : '.',
path.relative(
relativeFrom,
path.join(this.rollupConfig.output.dir, this.inputFiles[src])
)
)
.replaceAll('\\', '/');
return this.scriptGenerator(importPath, eleventyInstance);
}
defaultScriptGenerator(filePath, eleventyInstance) {
return `<script src="${filePath}" type="module"></script>`;
}
/**
* Calculates the inputs for Rollup
*
* This handles combining possible preexisting input configs with the ones generated by this plugin
*
* @returns {string[]} List of inputs
*/
async getRollupInputs() {
await this.rollupConfigPromise;
const pluginInputs = Object.keys(this.inputFiles);
// No other inputs defined
if (!('input' in this.rollupConfig)) {
return pluginInputs;
}
// Input is simple array
if (Array.isArray(this.rollupConfig.input)) {
return [...this.rollupConfig.input, ...pluginInputs];
}
// Input is the complex object form
if (typeof this.rollupConfig.input === 'object') {
const res = {};
Object.assign(res, this.rollupConfig.input);
for (const entry of pluginInputs) {
res[entry] = entry;
}
return res;
}
// Input is just a single string
return [this.rollupConfig.input, ...pluginInputs];
}
/**
* After the "normal" eleventy build is done, we need to start the compile step of rollup.
* At this point we know all dependencies and can start building.
*/
async afterBuild() {
// If we run in serverless, we don't want to write to the filesystem
if (process.env.ELEVENTY_SERVERLESS) {
return;
}
await this.rollupConfigPromise;
// Return early if no JS was used, since rollup throws on empty inputs
if (!Object.keys(this.inputFiles).length) {
return;
}
// Overwrite the rollup input argument to contain the shortcode entrypoints
const input = await this.getRollupInputs();
// We import here, because we don't need rollup anywhere else and it shouldn't
// load in serverless environments.
const rollup = require('rollup');
const bundle = await rollup.rollup({
input,
...this.rollupConfig,
});
await bundle.write({
entryFileNames: (chunk) => {
const src = path.relative('.', chunk.facadeModuleId);
return this.inputFiles[src];
},
...this.rollupConfig.output,
});
await bundle.close();
}
}