123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- /**
- * Copyright 2018 Google Inc. All Rights Reserved.
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- "use strict";
- const { readFileSync } = require("fs");
- const { join } = require("path");
- const ejs = require("ejs");
- const MagicString = require("magic-string");
- const json5 = require("json5");
- // See https://github.com/surma/rollup-plugin-off-main-thread/issues/49
- const matchAll = require("string.prototype.matchall");
- const defaultOpts = {
- // A string containing the EJS template for the amd loader. If `undefined`,
- // OMT will use `loader.ejs`.
- loader: readFileSync(join(__dirname, "/loader.ejs"), "utf8"),
- // Use `fetch()` + `eval()` to load dependencies instead of `<script>` tags
- // and `importScripts()`. _This is not CSP compliant, but is required if you
- // want to use dynamic imports in ServiceWorker_.
- useEval: false,
- // Function name to use instead of AMD’s `define`.
- amdFunctionName: "define",
- // A function that determines whether the loader code should be prepended to a
- // certain chunk. Should return true if the load is supposed to be prepended.
- prependLoader: (chunk, workerFiles) =>
- chunk.isEntry || workerFiles.includes(chunk.facadeModuleId),
- // The scheme used when importing workers as a URL.
- urlLoaderScheme: "omt",
- // Silence the warning about ESM being badly supported in workers.
- silenceESMWorkerWarning: false
- };
- // A regexp to find static `new Worker` invocations.
- // Matches `new Worker(...file part...`
- // File part matches one of:
- // - '...'
- // - "..."
- // - `import.meta.url`
- // - new URL('...', import.meta.url)
- // - new URL("...", import.meta.url)
- const workerRegexpForTransform = /(new\s+Worker\()\s*(('.*?'|".*?")|import\.meta\.url|new\s+URL\(('.*?'|".*?"),\s*import\.meta\.url\))/gs;
- // A regexp to find static `new Worker` invocations we've rewritten during the transform phase.
- // Matches `new Worker(...file part..., ...options...`.
- // File part matches one of:
- // - new URL('...', module.uri)
- // - new URL("...", module.uri)
- const workerRegexpForOutput = /new\s+Worker\(new\s+URL\((?:'.*?'|".*?"),\s*module\.uri\)\s*(,([^)]+))/gs;
- let longWarningAlreadyShown = false;
- module.exports = function(opts = {}) {
- opts = Object.assign({}, defaultOpts, opts);
- opts.loader = ejs.render(opts.loader, opts);
- const urlLoaderPrefix = opts.urlLoaderScheme + ":";
- let workerFiles;
- let isEsmOutput = () => { throw new Error("outputOptions hasn't been called yet") };
- return {
- name: "off-main-thread",
- async buildStart(options) {
- workerFiles = [];
- },
- async resolveId(id, importer) {
- if (!id.startsWith(urlLoaderPrefix)) return;
- const path = id.slice(urlLoaderPrefix.length);
- const resolved = await this.resolve(path, importer);
- if (!resolved)
- throw Error(`Cannot find module '${path}' from '${importer}'`);
- const newId = resolved.id;
- return urlLoaderPrefix + newId;
- },
- load(id) {
- if (!id.startsWith(urlLoaderPrefix)) return;
- const realId = id.slice(urlLoaderPrefix.length);
- const chunkRef = this.emitFile({ id: realId, type: "chunk" });
- return `export default import.meta.ROLLUP_FILE_URL_${chunkRef};`;
- },
- async transform(code, id) {
- const ms = new MagicString(code);
- const replacementPromises = [];
- for (const match of matchAll(code, workerRegexpForTransform)) {
- let [
- fullMatch,
- partBeforeArgs,
- workerSource,
- directWorkerFile,
- workerFile,
- ] = match;
- const workerParametersEndIndex = match.index + fullMatch.length;
- const matchIndex = match.index;
- const workerParametersStartIndex = matchIndex + partBeforeArgs.length;
- let workerIdPromise;
- if (workerSource === "import.meta.url") {
- // Turn the current file into a chunk
- workerIdPromise = Promise.resolve(id);
- } else {
- // Otherwise it's a string literal either directly or in the `new URL(...)`.
- if (directWorkerFile) {
- const fullMatchWithOpts = `${fullMatch}, …)`;
- const fullReplacement = `new Worker(new URL(${directWorkerFile}, import.meta.url), …)`;
- if (!longWarningAlreadyShown) {
- this.warn(
- `rollup-plugin-off-main-thread:
- \`${fullMatchWithOpts}\` suggests that the Worker should be relative to the document, not the script.
- In 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.
- This might lead to incorrect behaviour during runtime.
- If you did mean to use a URL relative to the current module, please change your code to the following form:
- \`${fullReplacement}\`
- This will become a hard error in the future.`,
- matchIndex
- );
- longWarningAlreadyShown = true;
- } else {
- this.warn(
- `rollup-plugin-off-main-thread: Treating \`${fullMatchWithOpts}\` as \`${fullReplacement}\``,
- matchIndex
- );
- }
- workerFile = directWorkerFile;
- }
- // Cut off surrounding quotes.
- workerFile = workerFile.slice(1, -1);
- if (!/^\.{1,2}\//.test(workerFile)) {
- let isError = false;
- if (directWorkerFile) {
- // If direct worker file, it must be in `./something` form.
- isError = true;
- } else {
- // If `new URL(...)` it can be in `new URL('something', import.meta.url)` form too,
- // so just check it's not absolute.
- if (/^(\/|https?:)/.test(workerFile)) {
- isError = true;
- } else {
- // If it does turn out to be `new URL('something', import.meta.url)` form,
- // prepend `./` so that it becomes valid module specifier.
- workerFile = `./${workerFile}`;
- }
- }
- if (isError) {
- this.warn(
- `Paths passed to the Worker constructor must be relative to the current file, i.e. start with ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".`,
- matchIndex
- );
- continue;
- }
- }
- workerIdPromise = this.resolve(workerFile, id).then(res => res.id);
- }
- replacementPromises.push(
- (async () => {
- const resolvedWorkerFile = await workerIdPromise;
- workerFiles.push(resolvedWorkerFile);
- const chunkRefId = this.emitFile({
- id: resolvedWorkerFile,
- type: "chunk"
- });
- ms.overwrite(
- workerParametersStartIndex,
- workerParametersEndIndex,
- `new URL(import.meta.ROLLUP_FILE_URL_${chunkRefId}, import.meta.url)`
- );
- })()
- );
- }
- // No matches found.
- if (!replacementPromises.length) {
- return;
- }
- // Wait for all the scheduled replacements to finish.
- await Promise.all(replacementPromises);
- return {
- code: ms.toString(),
- map: ms.generateMap({ hires: true })
- };
- },
- resolveFileUrl(chunk) {
- return JSON.stringify(chunk.relativePath);
- },
- outputOptions({ format }) {
- if (format === "esm" || format === "es") {
- if (!opts.silenceESMWorkerWarning) {
- this.warn(
- 'Very few browsers support ES modules in Workers. If you want to your code to run in all browsers, set `output.format = "amd";`'
- );
- }
- // In ESM, we never prepend a loader.
- isEsmOutput = () => true;
- } else if (format !== "amd") {
- this.error(
- `\`output.format\` must either be "amd" or "esm", got "${format}"`
- );
- } else {
- isEsmOutput = () => false;
- }
- },
- renderDynamicImport() {
- if (isEsmOutput()) return;
- // In our loader, `require` simply return a promise directly.
- // This is tinier and simpler output than the Rollup's default.
- return {
- left: 'require(',
- right: ')'
- };
- },
- resolveImportMeta(property) {
- if (isEsmOutput()) return;
- if (property === 'url') {
- // In our loader, `module.uri` is already fully resolved
- // so we can emit something shorter than the Rollup's default.
- return `module.uri`;
- }
- },
- renderChunk(code, chunk, outputOptions) {
- // We don’t need to do any loader processing when targeting ESM format.
- if (isEsmOutput()) return;
- if (outputOptions.banner && outputOptions.banner.length > 0) {
- this.error(
- "OMT currently doesn’t work with `banner`. Feel free to submit a PR at https://github.com/surma/rollup-plugin-off-main-thread"
- );
- return;
- }
- const ms = new MagicString(code);
- for (const match of matchAll(code, workerRegexpForOutput)) {
- let [fullMatch, optionsWithCommaStr, optionsStr] = match;
- let options;
- try {
- options = json5.parse(optionsStr);
- } catch (e) {
- // If we couldn't parse the options object, maybe it's something dynamic or has nested
- // parentheses or something like that. In that case, treat it as a warning
- // and not a hard error, just like we wouldn't break on unmatched regex.
- console.warn("Couldn't match options object", fullMatch, ": ", e);
- continue;
- }
- if (!("type" in options)) {
- // Nothing to do.
- continue;
- }
- delete options.type;
- const replacementEnd = match.index + fullMatch.length;
- const replacementStart = replacementEnd - optionsWithCommaStr.length;
- optionsStr = json5.stringify(options);
- optionsWithCommaStr = optionsStr === "{}" ? "" : `, ${optionsStr}`;
- ms.overwrite(
- replacementStart,
- replacementEnd,
- optionsWithCommaStr
- );
- }
- // Mangle define() call
- ms.remove(0, "define(".length);
- // If the module does not have any dependencies, it’s technically okay
- // to skip the dependency array. But our minimal loader expects it, so
- // we add it back in.
- if (!code.startsWith("define([")) {
- ms.prepend("[],");
- }
- ms.prepend(`${opts.amdFunctionName}(`);
- // Prepend loader if it’s an entry point or a worker file
- if (opts.prependLoader(chunk, workerFiles)) {
- ms.prepend(opts.loader);
- }
- const newCode = ms.toString();
- const hasCodeChanged = code !== newCode;
- return {
- code: newCode,
- // Avoid generating sourcemaps if possible as it can be a very expensive operation
- map: hasCodeChanged ? ms.generateMap({ hires: true }) : null
- };
- }
- };
- };
|