extension_helpers.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. var path = require('path');
  4. var walkSync = require('walk-sync');
  5. /**
  6. Helper scripts to be used by extension authors (and extension extenders) in a
  7. webpack.config.json to create builds that do not include upstream extensions.
  8. Inspects the package.json of the user's package and those of its dependencies
  9. to find extensions that should be excluded.
  10. Slightly more than minimal valid setup in package.json:
  11. {
  12. "name": "foo-widget",
  13. "jupyter": {
  14. "lab": {
  15. "main": "lab-extension.js"
  16. }
  17. },
  18. "dependencies": {
  19. "jupyterlab": "*",
  20. "jupyter-js-widgets": "*"
  21. }
  22. }
  23. Example usage in webpack.config.js:
  24. var jlab_helpers = require('jupyterlab/scripts/extension_helpers');
  25. module.exports = [{
  26. entry: './src/lab/extension.js',
  27. output: {
  28. filename: 'lab-extension.js',
  29. path: '../pythonpkg/static',
  30. libraryTarget: 'this'
  31. },
  32. externals: jlab_helpers.upstreamExternals(require)
  33. }];
  34. */
  35. /**
  36. * Determine if a string starts with another.
  37. *
  38. * @param str (string) - the string possibly starting with the substring.
  39. *
  40. * @param query (string) - the substring whose presence we are testing.
  41. *
  42. * @returns true if str starts with query
  43. *
  44. * #### Notes
  45. * This is a cross-browser version of String.prototype.startsWith
  46. */
  47. function startsWith(str, query) {
  48. return str.lastIndexOf(query, 0) === 0;
  49. }
  50. /**
  51. * Create a Webpack `externals` function for a shimmed external package.
  52. *
  53. * @param pkgName (string) - The name of the package
  54. *
  55. * @returns A function to be used as part of a WebPack config.
  56. */
  57. function createShimHandler(pkgName) {
  58. return function(context, request, callback) {
  59. // TODO: better path regex, probably only looking for .js or .css
  60. // since that is all we save
  61. if (startsWith(request, pkgName)) {
  62. try {
  63. var path = require.resolve(request);
  64. } catch (err) {
  65. return callback(err);
  66. }
  67. var index = path.indexOf(request);
  68. path = path.slice(index + pkgName.length);
  69. if (path.indexOf('/') === 0) {
  70. path = path.slice(1);
  71. }
  72. var shim = 'var jupyter.externals["' + pkgName + '"]["' + path + '"]';
  73. return callback(null, shim);
  74. }
  75. callback();
  76. }
  77. }
  78. /**
  79. * Create a shim to export all of a library's modules to a namespaced object.
  80. *
  81. * @param modName (string) - The name of the module to shim.
  82. *
  83. * @param sourceFolder (string) - The source folder (defaults to `lib`).
  84. *
  85. * @returns The code used to export the entire package.
  86. */
  87. function createShim(modName, sourceFolder) {
  88. var dirs = [];
  89. var files = [];
  90. var lines = ['var shim = {};'];
  91. // Find the path to the module.
  92. var modPath = require.resolve(modName + '/package.json');
  93. sourceFolder = sourceFolder || 'lib';
  94. modPath = path.posix.join(path.dirname(modPath), sourceFolder);
  95. // Walk through the source tree.
  96. var entries = walkSync.entries(modPath, {
  97. directories: false,
  98. globs: ['**/*.js', '**/*.css']
  99. });
  100. for (var i = 0; i < entries.length; i++) {
  101. // Get the relative path to the entry.
  102. var entryPath = path.posix.join(sourceFolder, entries[i].relativePath);
  103. // Add an entries for each file.
  104. lines.push('shim["' + entryPath + '"] = require("' + path.posix.join(modName, entryPath) + '");');
  105. }
  106. lines.push('module.exports = shim;');
  107. return lines.join('\n');
  108. }
  109. /**
  110. * Determine whether a package is a JupyterLab extension.
  111. *
  112. * @param pkg (string) - The package.json object.
  113. *
  114. * @returns true if the package is a JupyterLab extension.
  115. */
  116. function isLabExtension(pkg){
  117. try {
  118. // for now, just try to load the key... could check whether file exists?
  119. pkg['jupyter']['lab']['main']
  120. return true;
  121. } catch(err) {
  122. return false;
  123. }
  124. }
  125. /**
  126. * Recurse through dependencies, collecting all external functions.
  127. *
  128. * @param function - the environment require function.
  129. *
  130. * @param onlyUpstream - if true, do not return externals provided by this package
  131. *
  132. * @returns an externals object to be used in a Webpack config.
  133. *
  134. * #### Notes
  135. * A sample Webpack config will look like
  136. *
  137. var jlab_helpers = require('jupyterlab/scripts/extension_helpers');
  138. module.exports = [{
  139. entry: './src/lab/extension.js',
  140. output: {
  141. filename: 'lab-extension.js',
  142. path: '../pythonpkg/static',
  143. libraryTarget: 'this'
  144. },
  145. externals: jlab_helpers.upstreamExternals(require)
  146. }];
  147. */
  148. function upstreamExternals(_require, onlyUpstream) {
  149. // Parse the externals of this package.
  150. // remember which packages we have seen
  151. var _seen = {};
  152. /**
  153. * Load the externals from a JupyterLab extension package.
  154. *
  155. * @param pkg_path (string) - the path on the filesystem to the package.
  156. *
  157. * @param pkg (object) - the package.json object.
  158. *
  159. * @returns an array containing the externals this package provides, excluding itself.
  160. *
  161. * #### Notes
  162. * This returns an array containing the externals referred to in the
  163. * package.json jupyter.lab.externals object:
  164. *
  165. * {module: "module-exporting-externals"}
  166. *
  167. * or
  168. *
  169. * {module: "module-exporting-externals", name: "exported-object"}
  170. */
  171. function _load_externals(pkg_path, pkg) {
  172. var externals = pkg.jupyter.lab.externals;
  173. if (externals) {
  174. try {
  175. var externalModule = _require(pkg_path + '/' + externals['module']);
  176. if (externals['name']) {
  177. pkgExternals = externalModule[externals['name']];
  178. } else {
  179. pkgExternals = externalModule;
  180. }
  181. } catch (err) {
  182. console.error('Error importing externals for ' + pkg.name);
  183. }
  184. }
  185. pkgExternals = pkgExternals || [];
  186. if (!Array.isArray(pkgExternals)) {
  187. pkgExternals = [pkgExternals];
  188. }
  189. return pkgExternals;
  190. }
  191. // return an array of strings, functions or regexen that can be deferenced by
  192. // webpack `externals` config directive
  193. // https://webpack.github.io/docs/configuration.html#externals
  194. function _find_externals(pkg_path, root) {
  195. var pkg = _require(pkg_path + '/package.json');
  196. var pkgName = pkg['name'];
  197. var lab_config;
  198. var externals = [];
  199. // only visit each named package once
  200. _seen[pkgName] = true;
  201. if (!isLabExtension(pkg)) {
  202. return [];
  203. }
  204. console.info("Inspecting " + pkgName + " for externals it provides...");
  205. if (!(root && onlyUpstream)) {
  206. externals.push.apply(externals, _load_externals(pkg_path, pkg, _require));
  207. }
  208. if (!root) {
  209. externals.push(createShimHandler(pkg['name']));
  210. }
  211. // Recurse through the dependencies, and collect externals
  212. // for JupyterLab extensions
  213. return Object.keys(pkg['dependencies'])
  214. .filter(function(depName){ return !_seen[depName]; })
  215. .reduce(function(externals, depName){
  216. return externals.concat(
  217. // We assume the node_modules is flat
  218. // TODO: actually change directory before doing _find_externals?
  219. _find_externals(depName));
  220. }, externals);
  221. }
  222. var externals = _find_externals(".", true);
  223. return externals;
  224. }
  225. module.exports = {
  226. upstreamExternals: upstreamExternals,
  227. isLabExtension: isLabExtension,
  228. createShim: createShim,
  229. createShimHandler: createShimHandler,
  230. };