extension_helpers.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. * Parse a Webpack request to a module that has been shimmed.
  37. *
  38. * @param basePath (string) - The base import path with a trailing slash.
  39. *
  40. * @param outName (string) - The name of the output variable.
  41. *
  42. * @param request (string) - The Webpack request path.
  43. *
  44. * @returns An appropriate mangled Webpack import line or `undefined`.
  45. */
  46. function parseShimmed(basePath, outName, request) {
  47. var regex = new RegExp("^" + basePath + '[\\w\.\\/-]+$');
  48. if (regex.test(request)) {
  49. try {
  50. var path = require.resolve(request);
  51. } catch (err) {
  52. return;
  53. }
  54. var index = path.indexOf(basePath);
  55. path = path.slice(index + basePath.length);
  56. return 'var ' + outName + '["' + path + '"]';
  57. }
  58. }
  59. /**
  60. * The base Webpack `externals` config.
  61. */
  62. var BASE_EXTERNALS = [
  63. {
  64. 'jquery': '$',
  65. 'jquery-ui': '$'
  66. },
  67. function(context, request, callback) {
  68. // All phosphor imports get mangled to use an external bundle.
  69. lib = parseShimmed('phosphor/lib/', 'jupyter.phosphor', request);
  70. if (lib) {
  71. return callback(null, lib);
  72. }
  73. // CodeMirror imports get mangled to use an external bundle.
  74. var codeMirrorPaths = [
  75. 'codemirror/mode/meta',
  76. 'codemirror', '../lib/codemirror', '../../lib/codemirror',
  77. 'codemirror/lib/codemirror.css'
  78. ];
  79. if (codeMirrorPaths.indexOf(request) !== -1) {
  80. return callback(null, 'var CodeMirror');
  81. }
  82. callback();
  83. }
  84. ];
  85. /**
  86. * The default Webpack `externals` config that should be applied to
  87. * extensions of JupyterLab.
  88. */
  89. var DEFAULT_EXTERNALS = BASE_EXTERNALS.concat([
  90. {
  91. 'jupyter-js-services': 'jupyter.services',
  92. },
  93. function(context, request, callback) {
  94. // JupyterLab imports get mangled to use an external bundle.
  95. var lib = parseShimmed('jupyterlab/lib/', 'jupyter.lab', request);
  96. if (lib) {
  97. return callback(null, lib);
  98. }
  99. callback();
  100. }
  101. ]);
  102. /**
  103. * Create a shim to export all of a library's modules to a namespaced object.
  104. *
  105. * @param modName (string) - The name of the module to shim.
  106. *
  107. * @param sourceFolder (string) - The source folder (defaults to `lib`).
  108. *
  109. * @returns The code used to export the entire package.
  110. */
  111. function createShim(modName, sourceFolder) {
  112. var dirs = [];
  113. var files = [];
  114. var lines = ['var ' + modName + ' = {};'];
  115. // Find the path to the module.
  116. var modPath = require.resolve(modName + '/package.json');
  117. sourceFolder = sourceFolder || 'lib';
  118. modPath = path.join(path.dirname(modPath), sourceFolder);
  119. // Walk through the source tree.
  120. var entries = walkSync.entries(modPath, {
  121. directories: false,
  122. globs: ['**/*.js', '**/*.css']
  123. });
  124. for (var i = 0; i < entries.length; i++) {
  125. // Get the relative path to the entry.
  126. var entryPath = entries[i].relativePath;
  127. // Add an entries for each file.
  128. lines.push(modName + '["' + entryPath + '"] = require("' + path.join(modName, sourceFolder, entryPath) + '");');
  129. }
  130. lines.push('module.exports = ' + modName + ';');
  131. return lines.join('\n');
  132. }
  133. // determine whether the package JSON contains a JupyterLab extension
  134. function validateExtension(pkg){
  135. try {
  136. // for now, just try to load the key... could check whether file exists?
  137. pkg['jupyter']['lab']['main']
  138. return true;
  139. } catch(err) {
  140. return false;
  141. }
  142. }
  143. // the publicly exposed function
  144. function upstreamExternals(_require) {
  145. // remember which packages we have seen
  146. var _seen = {},
  147. // load the user's package.json
  148. _user_pkg = _require('./package.json');
  149. // check for whether this is the root package
  150. function _is_user_pkg(pkg) {
  151. return _user_pkg['name'] === pkg['name'];
  152. }
  153. // use the provided scoped _require and the current nested location
  154. // in the `node_modules` hierarchy to resolve down to the list of externals
  155. function _load_externals(pkg_path, pkg) {
  156. var pkg_externals = [pkg['name']];
  157. try {
  158. pkg_externals = pkg_externals.concat(_require(
  159. pkg_path + '/' + pkg['jupyter']['lab']['externals']));
  160. } catch (err) {
  161. // not really worth adding any output here... usually, just the name will
  162. // suffice
  163. }
  164. return pkg_externals || [];
  165. }
  166. // return an array of strings, functions or regexen that can be deferenced by
  167. // webpack `externals` config directive
  168. // https://webpack.github.io/docs/configuration.html#externals
  169. function _find_externals(pkg_path) {
  170. var pkg = _require(pkg_path + '/package.json'),
  171. lab_config;
  172. // only visit each named package once
  173. _seen[pkg['name']] = true;
  174. if (!validateExtension(pkg)) {
  175. if (!_is_user_pkg(pkg)) {
  176. return [];
  177. } else {
  178. throw Error(
  179. pkg['name'] + ' does not contain a jupyter configuration. ' +
  180. ' Please see TODO: where?'
  181. );
  182. }
  183. }
  184. console.info("Inspecting", pkg['name'],
  185. "for upstream JupyterLab extensions...");
  186. // ok, actually start building the externals. If it is the user package,
  187. // it SHOULDN'T be an external, as this is what the user will use for their
  188. // build... otherwise, load the externals, which is probably
  189. var externals = _is_user_pkg(pkg) ?
  190. DEFAULT_EXTERNALS :
  191. _load_externals(pkg_path, pkg, _require);
  192. // Recurse through the dependencies, and collect anything that has
  193. // a JupyterLab config
  194. return Object.keys(pkg['dependencies'])
  195. .filter(function(key){ return !_seen[key]; })
  196. .reduce(function(externals, dep_name){
  197. return externals.concat(
  198. _find_externals(pkg_path + '/node_modules/' + dep_name));
  199. }, externals);
  200. }
  201. return _find_externals(".");
  202. }
  203. module.exports = {
  204. upstreamExternals: upstreamExternals,
  205. validateExtension: validateExtension,
  206. createShim: createShim,
  207. parseShimmed: parseShimmed,
  208. BASE_EXTERNALS: BASE_EXTERNALS,
  209. DEFAULT_EXTERNALS: DEFAULT_EXTERNALS,
  210. };