source: frontend/node_modules/@surma/rollup-plugin-off-main-thread/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 11.2 KB
Line 
1/**
2 * Copyright 2018 Google Inc. All Rights Reserved.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 * http://www.apache.org/licenses/LICENSE-2.0
7 * Unless required by applicable law or agreed to in writing, software
8 * distributed under the License is distributed on an "AS IS" BASIS,
9 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 * See the License for the specific language governing permissions and
11 * limitations under the License.
12 */
13
14"use strict";
15
16const { readFileSync } = require("fs");
17const { join } = require("path");
18const ejs = require("ejs");
19const MagicString = require("magic-string");
20const json5 = require("json5");
21// See https://github.com/surma/rollup-plugin-off-main-thread/issues/49
22const matchAll = require("string.prototype.matchall");
23
24const defaultOpts = {
25 // A string containing the EJS template for the amd loader. If `undefined`,
26 // OMT will use `loader.ejs`.
27 loader: readFileSync(join(__dirname, "/loader.ejs"), "utf8"),
28 // Use `fetch()` + `eval()` to load dependencies instead of `<script>` tags
29 // and `importScripts()`. _This is not CSP compliant, but is required if you
30 // want to use dynamic imports in ServiceWorker_.
31 useEval: false,
32 // Function name to use instead of AMD’s `define`.
33 amdFunctionName: "define",
34 // A function that determines whether the loader code should be prepended to a
35 // certain chunk. Should return true if the load is supposed to be prepended.
36 prependLoader: (chunk, workerFiles) =>
37 chunk.isEntry || workerFiles.includes(chunk.facadeModuleId),
38 // The scheme used when importing workers as a URL.
39 urlLoaderScheme: "omt",
40 // Silence the warning about ESM being badly supported in workers.
41 silenceESMWorkerWarning: false
42};
43
44// A regexp to find static `new Worker` invocations.
45// Matches `new Worker(...file part...`
46// File part matches one of:
47// - '...'
48// - "..."
49// - `import.meta.url`
50// - new URL('...', import.meta.url)
51// - new URL("...", import.meta.url)
52const workerRegexpForTransform = /(new\s+Worker\()\s*(('.*?'|".*?")|import\.meta\.url|new\s+URL\(('.*?'|".*?"),\s*import\.meta\.url\))/gs;
53
54// A regexp to find static `new Worker` invocations we've rewritten during the transform phase.
55// Matches `new Worker(...file part..., ...options...`.
56// File part matches one of:
57// - new URL('...', module.uri)
58// - new URL("...", module.uri)
59const workerRegexpForOutput = /new\s+Worker\(new\s+URL\((?:'.*?'|".*?"),\s*module\.uri\)\s*(,([^)]+))/gs;
60
61let longWarningAlreadyShown = false;
62
63module.exports = function(opts = {}) {
64 opts = Object.assign({}, defaultOpts, opts);
65
66 opts.loader = ejs.render(opts.loader, opts);
67
68 const urlLoaderPrefix = opts.urlLoaderScheme + ":";
69
70 let workerFiles;
71 let isEsmOutput = () => { throw new Error("outputOptions hasn't been called yet") };
72 return {
73 name: "off-main-thread",
74
75 async buildStart(options) {
76 workerFiles = [];
77 },
78
79 async resolveId(id, importer) {
80 if (!id.startsWith(urlLoaderPrefix)) return;
81
82 const path = id.slice(urlLoaderPrefix.length);
83 const resolved = await this.resolve(path, importer);
84 if (!resolved)
85 throw Error(`Cannot find module '${path}' from '${importer}'`);
86 const newId = resolved.id;
87
88 return urlLoaderPrefix + newId;
89 },
90
91 load(id) {
92 if (!id.startsWith(urlLoaderPrefix)) return;
93
94 const realId = id.slice(urlLoaderPrefix.length);
95 const chunkRef = this.emitFile({ id: realId, type: "chunk" });
96 return `export default import.meta.ROLLUP_FILE_URL_${chunkRef};`;
97 },
98
99 async transform(code, id) {
100 const ms = new MagicString(code);
101
102 const replacementPromises = [];
103
104 for (const match of matchAll(code, workerRegexpForTransform)) {
105 let [
106 fullMatch,
107 partBeforeArgs,
108 workerSource,
109 directWorkerFile,
110 workerFile,
111 ] = match;
112
113 const workerParametersEndIndex = match.index + fullMatch.length;
114 const matchIndex = match.index;
115 const workerParametersStartIndex = matchIndex + partBeforeArgs.length;
116
117 let workerIdPromise;
118 if (workerSource === "import.meta.url") {
119 // Turn the current file into a chunk
120 workerIdPromise = Promise.resolve(id);
121 } else {
122 // Otherwise it's a string literal either directly or in the `new URL(...)`.
123 if (directWorkerFile) {
124 const fullMatchWithOpts = `${fullMatch}, …)`;
125 const fullReplacement = `new Worker(new URL(${directWorkerFile}, import.meta.url), …)`;
126
127 if (!longWarningAlreadyShown) {
128 this.warn(
129 `rollup-plugin-off-main-thread:
130\`${fullMatchWithOpts}\` suggests that the Worker should be relative to the document, not the script.
131In the bundler, we don't know what the final document's URL will be, and instead assume it's a URL relative to the current module.
132This might lead to incorrect behaviour during runtime.
133If you did mean to use a URL relative to the current module, please change your code to the following form:
134\`${fullReplacement}\`
135This will become a hard error in the future.`,
136 matchIndex
137 );
138 longWarningAlreadyShown = true;
139 } else {
140 this.warn(
141 `rollup-plugin-off-main-thread: Treating \`${fullMatchWithOpts}\` as \`${fullReplacement}\``,
142 matchIndex
143 );
144 }
145 workerFile = directWorkerFile;
146 }
147
148 // Cut off surrounding quotes.
149 workerFile = workerFile.slice(1, -1);
150
151 if (!/^\.{1,2}\//.test(workerFile)) {
152 let isError = false;
153 if (directWorkerFile) {
154 // If direct worker file, it must be in `./something` form.
155 isError = true;
156 } else {
157 // If `new URL(...)` it can be in `new URL('something', import.meta.url)` form too,
158 // so just check it's not absolute.
159 if (/^(\/|https?:)/.test(workerFile)) {
160 isError = true;
161 } else {
162 // If it does turn out to be `new URL('something', import.meta.url)` form,
163 // prepend `./` so that it becomes valid module specifier.
164 workerFile = `./${workerFile}`;
165 }
166 }
167 if (isError) {
168 this.warn(
169 `Paths passed to the Worker constructor must be relative to the current file, i.e. start with ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".`,
170 matchIndex
171 );
172 continue;
173 }
174 }
175
176 workerIdPromise = this.resolve(workerFile, id).then(res => res.id);
177 }
178
179 replacementPromises.push(
180 (async () => {
181 const resolvedWorkerFile = await workerIdPromise;
182 workerFiles.push(resolvedWorkerFile);
183 const chunkRefId = this.emitFile({
184 id: resolvedWorkerFile,
185 type: "chunk"
186 });
187
188 ms.overwrite(
189 workerParametersStartIndex,
190 workerParametersEndIndex,
191 `new URL(import.meta.ROLLUP_FILE_URL_${chunkRefId}, import.meta.url)`
192 );
193 })()
194 );
195 }
196
197 // No matches found.
198 if (!replacementPromises.length) {
199 return;
200 }
201
202 // Wait for all the scheduled replacements to finish.
203 await Promise.all(replacementPromises);
204
205 return {
206 code: ms.toString(),
207 map: ms.generateMap({ hires: true })
208 };
209 },
210
211 resolveFileUrl(chunk) {
212 return JSON.stringify(chunk.relativePath);
213 },
214
215 outputOptions({ format }) {
216 if (format === "esm" || format === "es") {
217 if (!opts.silenceESMWorkerWarning) {
218 this.warn(
219 'Very few browsers support ES modules in Workers. If you want to your code to run in all browsers, set `output.format = "amd";`'
220 );
221 }
222 // In ESM, we never prepend a loader.
223 isEsmOutput = () => true;
224 } else if (format !== "amd") {
225 this.error(
226 `\`output.format\` must either be "amd" or "esm", got "${format}"`
227 );
228 } else {
229 isEsmOutput = () => false;
230 }
231 },
232
233 renderDynamicImport() {
234 if (isEsmOutput()) return;
235
236 // In our loader, `require` simply return a promise directly.
237 // This is tinier and simpler output than the Rollup's default.
238 return {
239 left: 'require(',
240 right: ')'
241 };
242 },
243
244 resolveImportMeta(property) {
245 if (isEsmOutput()) return;
246
247 if (property === 'url') {
248 // In our loader, `module.uri` is already fully resolved
249 // so we can emit something shorter than the Rollup's default.
250 return `module.uri`;
251 }
252 },
253
254 renderChunk(code, chunk, outputOptions) {
255 // We don’t need to do any loader processing when targeting ESM format.
256 if (isEsmOutput()) return;
257
258 if (outputOptions.banner && outputOptions.banner.length > 0) {
259 this.error(
260 "OMT currently doesn’t work with `banner`. Feel free to submit a PR at https://github.com/surma/rollup-plugin-off-main-thread"
261 );
262 return;
263 }
264 const ms = new MagicString(code);
265
266 for (const match of matchAll(code, workerRegexpForOutput)) {
267 let [fullMatch, optionsWithCommaStr, optionsStr] = match;
268 let options;
269 try {
270 options = json5.parse(optionsStr);
271 } catch (e) {
272 // If we couldn't parse the options object, maybe it's something dynamic or has nested
273 // parentheses or something like that. In that case, treat it as a warning
274 // and not a hard error, just like we wouldn't break on unmatched regex.
275 console.warn("Couldn't match options object", fullMatch, ": ", e);
276 continue;
277 }
278 if (!("type" in options)) {
279 // Nothing to do.
280 continue;
281 }
282 delete options.type;
283 const replacementEnd = match.index + fullMatch.length;
284 const replacementStart = replacementEnd - optionsWithCommaStr.length;
285 optionsStr = json5.stringify(options);
286 optionsWithCommaStr = optionsStr === "{}" ? "" : `, ${optionsStr}`;
287 ms.overwrite(
288 replacementStart,
289 replacementEnd,
290 optionsWithCommaStr
291 );
292 }
293
294 // Mangle define() call
295 ms.remove(0, "define(".length);
296 // If the module does not have any dependencies, it’s technically okay
297 // to skip the dependency array. But our minimal loader expects it, so
298 // we add it back in.
299 if (!code.startsWith("define([")) {
300 ms.prepend("[],");
301 }
302 ms.prepend(`${opts.amdFunctionName}(`);
303
304 // Prepend loader if it’s an entry point or a worker file
305 if (opts.prependLoader(chunk, workerFiles)) {
306 ms.prepend(opts.loader);
307 }
308
309 const newCode = ms.toString();
310 const hasCodeChanged = code !== newCode;
311 return {
312 code: newCode,
313 // Avoid generating sourcemaps if possible as it can be a very expensive operation
314 map: hasCodeChanged ? ms.generateMap({ hires: true }) : null
315 };
316 }
317 };
318};
Note: See TracBrowser for help on using the repository browser.