build.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import MiniCssExtractPlugin = require('mini-css-extract-plugin');
  6. import * as webpack from 'webpack';
  7. import * as fs from 'fs-extra';
  8. import * as glob from 'glob';
  9. import * as path from 'path';
  10. import * as utils from './utils';
  11. /**
  12. * A namespace for JupyterLab build utilities.
  13. */
  14. export namespace Build {
  15. /**
  16. * The options used to ensure a root package has the appropriate
  17. * assets for its JupyterLab extension packages.
  18. */
  19. export interface IEnsureOptions {
  20. /**
  21. * The output directory where the build assets should reside.
  22. */
  23. output: string;
  24. /**
  25. * The names of the packages to ensure.
  26. */
  27. packageNames: ReadonlyArray<string>;
  28. }
  29. /**
  30. * The JupyterLab extension attributes in a module.
  31. */
  32. export interface ILabExtension {
  33. /**
  34. * Indicates whether the extension is a standalone extension.
  35. *
  36. * #### Notes
  37. * If `true`, the `main` export of the package is used. If set to a string
  38. * path, the export from that path is loaded as a JupyterLab extension. It
  39. * is possible for one package to have both an `extension` and a
  40. * `mimeExtension` but they cannot be identical (i.e., the same export
  41. * cannot be declared both an `extension` and a `mimeExtension`).
  42. */
  43. readonly extension?: boolean | string;
  44. /**
  45. * Indicates whether the extension is a MIME renderer extension.
  46. *
  47. * #### Notes
  48. * If `true`, the `main` export of the package is used. If set to a string
  49. * path, the export from that path is loaded as a JupyterLab extension. It
  50. * is possible for one package to have both an `extension` and a
  51. * `mimeExtension` but they cannot be identical (i.e., the same export
  52. * cannot be declared both an `extension` and a `mimeExtension`).
  53. */
  54. readonly mimeExtension?: boolean | string;
  55. /**
  56. * The local schema file path in the extension package.
  57. */
  58. readonly schemaDir?: string;
  59. /**
  60. * The local theme file path in the extension package.
  61. */
  62. readonly themePath?: string;
  63. }
  64. /**
  65. * A minimal definition of a module's package definition (i.e., package.json).
  66. */
  67. export interface IModule {
  68. /**
  69. * The JupyterLab metadata/
  70. */
  71. jupyterlab?: ILabExtension;
  72. /**
  73. * The main entry point in a module.
  74. */
  75. main?: string;
  76. /**
  77. * The name of a module.
  78. */
  79. name: string;
  80. }
  81. /**
  82. * Ensures that the assets of plugin packages are populated for a build.
  83. *
  84. * @ Returns An array of lab extension config data.
  85. */
  86. export function ensureAssets(
  87. options: IEnsureOptions
  88. ): webpack.Configuration[] {
  89. let { output, packageNames } = options;
  90. const themeConfig: webpack.Configuration[] = [];
  91. packageNames.forEach(name => {
  92. const packageDataPath = require.resolve(path.join(name, 'package.json'));
  93. const packageDir = path.dirname(packageDataPath);
  94. const packageData = utils.readJSONFile(packageDataPath);
  95. const extension = normalizeExtension(packageData);
  96. const { schemaDir, themePath } = extension;
  97. // Handle schemas.
  98. if (schemaDir) {
  99. const schemas = glob.sync(
  100. path.join(path.join(packageDir, schemaDir), '*')
  101. );
  102. const destination = path.join(output, 'schemas', name);
  103. // Remove the existing directory if necessary.
  104. if (fs.existsSync(destination)) {
  105. try {
  106. const oldPackagePath = path.join(destination, 'package.json.orig');
  107. const oldPackageData = utils.readJSONFile(oldPackagePath);
  108. if (oldPackageData.version === packageData.version) {
  109. fs.removeSync(destination);
  110. }
  111. } catch (e) {
  112. fs.removeSync(destination);
  113. }
  114. }
  115. // Make sure the schema directory exists.
  116. fs.mkdirpSync(destination);
  117. // Copy schemas.
  118. schemas.forEach(schema => {
  119. const file = path.basename(schema);
  120. fs.copySync(schema, path.join(destination, file));
  121. });
  122. // Write the package.json file for future comparison.
  123. fs.copySync(
  124. path.join(packageDir, 'package.json'),
  125. path.join(destination, 'package.json.orig')
  126. );
  127. }
  128. if (!themePath) {
  129. return;
  130. }
  131. themeConfig.push({
  132. mode: 'production',
  133. entry: {
  134. index: path.join(name, themePath)
  135. },
  136. output: {
  137. path: path.resolve(path.join(output, 'themes', name)),
  138. // we won't use these JS files, only the extracted CSS
  139. filename: '[name].js'
  140. },
  141. module: {
  142. rules: [
  143. {
  144. test: /\.css$/,
  145. use: [MiniCssExtractPlugin.loader, 'css-loader']
  146. },
  147. {
  148. test: /\.svg/,
  149. use: [
  150. { loader: 'svg-url-loader', options: {} },
  151. { loader: 'svgo-loader', options: { plugins: [] } }
  152. ]
  153. },
  154. {
  155. test: /\.(png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
  156. use: [{ loader: 'url-loader', options: { limit: 10000 } }]
  157. }
  158. ]
  159. },
  160. plugins: [
  161. new MiniCssExtractPlugin({
  162. // Options similar to the same options in webpackOptions.output
  163. // both options are optional
  164. filename: '[name].css',
  165. chunkFilename: '[id].css'
  166. })
  167. ]
  168. });
  169. });
  170. return themeConfig;
  171. }
  172. /**
  173. * Returns JupyterLab extension metadata from a module.
  174. */
  175. export function normalizeExtension(module: IModule): ILabExtension {
  176. let { jupyterlab, main, name } = module;
  177. main = main || 'index.js';
  178. if (!jupyterlab) {
  179. throw new Error(`Module ${name} does not contain JupyterLab metadata.`);
  180. }
  181. let { extension, mimeExtension, schemaDir, themePath } = jupyterlab;
  182. extension = extension === true ? main : extension;
  183. mimeExtension = mimeExtension === true ? main : mimeExtension;
  184. if (extension && mimeExtension && extension === mimeExtension) {
  185. const message = 'extension and mimeExtension cannot be the same export.';
  186. throw new Error(message);
  187. }
  188. return { extension, mimeExtension, schemaDir, themePath };
  189. }
  190. }