index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. const { readFileSync } = require("fs");
  14. const { join } = require("path");
  15. const ejs = require("ejs");
  16. const MagicString = require("magic-string");
  17. const defaultOpts = {
  18. // A string containing the EJS template for the amd loader. If `undefined`,
  19. // OMT will use `loader.ejs`.
  20. loader: readFileSync(join(__dirname, "/loader.ejs"), "utf8"),
  21. // Use `fetch()` + `eval()` to load dependencies instead of `<script>` tags
  22. // and `importScripts()`. _This is not CSP compliant, but is required if you
  23. // want to use dynamic imports in ServiceWorker_.
  24. useEval: false,
  25. // A RegExp to find `new Workers()` calls. The second capture group _must_
  26. // capture the provided file name without the quotes.
  27. workerRegexp: /new Worker\((["'])(.+?)\1(,[^)]+)?\)/g,
  28. // Function name to use instead of AMD’s `define`.
  29. amdFunctionName: "define",
  30. // A function that determines whether the loader code should be prepended to a
  31. // certain chunk. Should return true if the load is supposed to be prepended.
  32. prependLoader: (chunk, workerFiles) =>
  33. chunk.isEntry || workerFiles.includes(chunk.facadeModuleId),
  34. // The scheme used when importing workers as a URL.
  35. urlLoaderScheme: "omt",
  36. // Silence the warning about ESM being badly supported in workers.
  37. silenceESMWorkerWarning: false,
  38. };
  39. module.exports = function(opts = {}) {
  40. opts = Object.assign({}, defaultOpts, opts);
  41. opts.loader = ejs.render(opts.loader, opts);
  42. const urlLoaderPrefix = opts.urlLoaderScheme + ":";
  43. let workerFiles;
  44. let isEsmOutput = false;
  45. return {
  46. name: "off-main-thread",
  47. async buildStart(options) {
  48. workerFiles = [];
  49. },
  50. outputOptions({ format }) {
  51. if ((format === "esm" || format === "es") && !opts.silenceESMWorkerWarning) {
  52. this.warn(
  53. 'Very few browsers support ES modules in Workers. If you want to your code to run in all browsers, set `output.format = "amd";`'
  54. );
  55. // In ESM, we never prepend a loader.
  56. isEsmOutput = true;
  57. } else if (format !== "amd") {
  58. this.error(
  59. `\`output.format\` must either be "amd" or "esm", got "${format}"`
  60. );
  61. }
  62. },
  63. async resolveId(id, importer) {
  64. if (!id.startsWith(urlLoaderPrefix)) return;
  65. const path = id.slice(urlLoaderPrefix.length);
  66. const resolved = await this.resolve(path, importer);
  67. if (!resolved) throw Error(`Cannot find module '${path}' from '${importer}'`);
  68. const newId = resolved.id;
  69. return urlLoaderPrefix + newId;
  70. },
  71. load(id) {
  72. if (!id.startsWith(urlLoaderPrefix)) return;
  73. const realId = id.slice(urlLoaderPrefix.length);
  74. const chunkRef = this.emitFile({ id: realId, type: "chunk" });
  75. return `export default import.meta.ROLLUP_FILE_URL_${chunkRef};`;
  76. },
  77. async transform(code, id) {
  78. // Copy the regexp as they are stateful and this hook is async.
  79. const workerRegexp = new RegExp(
  80. opts.workerRegexp.source,
  81. opts.workerRegexp.flags
  82. );
  83. if (!workerRegexp.test(code)) {
  84. return;
  85. }
  86. const ms = new MagicString(code);
  87. // Reset the regexp
  88. workerRegexp.lastIndex = 0;
  89. while (true) {
  90. const match = workerRegexp.exec(code);
  91. if (!match) {
  92. break;
  93. }
  94. const workerFile = match[2];
  95. let optionsObject = {};
  96. // Parse the optional options object
  97. if (match[3] && match[3].length > 0) {
  98. // FIXME: ooooof!
  99. optionsObject = new Function(`return ${match[3].slice(1)};`)();
  100. }
  101. if (!isEsmOutput) {
  102. delete optionsObject.type;
  103. }
  104. if (!new RegExp("^.*/").test(workerFile)) {
  105. this.warn(
  106. `Paths passed to the Worker constructor must be relative or absolute, i.e. start with /, ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".`
  107. );
  108. continue;
  109. }
  110. const resolvedWorkerFile = (await this.resolve(workerFile, id)).id;
  111. workerFiles.push(resolvedWorkerFile);
  112. const chunkRefId = this.emitFile({
  113. id: resolvedWorkerFile,
  114. type: "chunk"
  115. });
  116. const workerParametersStartIndex = match.index + "new Worker(".length;
  117. const workerParametersEndIndex =
  118. match.index + match[0].length - ")".length;
  119. ms.overwrite(
  120. workerParametersStartIndex,
  121. workerParametersEndIndex,
  122. `import.meta.ROLLUP_FILE_URL_${chunkRefId}, ${JSON.stringify(
  123. optionsObject
  124. )}`
  125. );
  126. }
  127. return {
  128. code: ms.toString(),
  129. map: ms.generateMap({ hires: true })
  130. };
  131. },
  132. resolveFileUrl(chunk) {
  133. return `"./${chunk.fileName}"`;
  134. },
  135. renderChunk(code, chunk, outputOptions) {
  136. // We don’t need to do any loader processing when targeting ESM format.
  137. if (isEsmOutput) {
  138. return;
  139. }
  140. if (outputOptions.banner && outputOptions.banner.length > 0) {
  141. this.error(
  142. "OMT currently doesn’t work with `banner`. Feel free to submit a PR at https://github.com/surma/rollup-plugin-off-main-thread"
  143. );
  144. return;
  145. }
  146. const ms = new MagicString(code);
  147. // Mangle define() call
  148. const id = `./${chunk.fileName}`;
  149. ms.remove(0, "define(".length);
  150. // If the module does not have any dependencies, it’s technically okay
  151. // to skip the dependency array. But our minimal loader expects it, so
  152. // we add it back in.
  153. if (!code.startsWith("define([")) {
  154. ms.prepend("[],");
  155. }
  156. ms.prepend(`${opts.amdFunctionName}("${id}",`);
  157. // Prepend loader if it’s an entry point or a worker file
  158. if (opts.prependLoader(chunk, workerFiles)) {
  159. ms.prepend(opts.loader);
  160. }
  161. return {
  162. code: ms.toString(),
  163. map: ms.generateMap({ hires: true })
  164. };
  165. }
  166. };
  167. };