build.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /* -----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import MiniCssExtractPlugin from '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. const { output, packageNames } = options;
  90. const themeConfig: webpack.Configuration[] = [];
  91. // Get the CSS imports.
  92. // We must import the application CSS first.
  93. // The order of the rest does not matter.
  94. // We explicitly ignore themes so they can be loaded dynamically.
  95. let cssImports: Array<string> = [];
  96. let appCSS = '';
  97. packageNames.forEach(name => {
  98. const packageDataPath = require.resolve(path.join(name, 'package.json'));
  99. const packageDir = path.dirname(packageDataPath);
  100. const data = utils.readJSONFile(packageDataPath);
  101. const extension = normalizeExtension(data);
  102. const { schemaDir, themePath } = extension;
  103. // Handle styles.
  104. if (data.style) {
  105. if (data.name === '@jupyterlab/application-extension') {
  106. appCSS = name + '/' + data.style;
  107. } else if (!data.jupyterlab.themePath) {
  108. cssImports.push(name + '/' + data.style);
  109. }
  110. }
  111. // Handle schemas.
  112. if (schemaDir) {
  113. const schemas = glob.sync(
  114. path.join(path.join(packageDir, schemaDir), '*')
  115. );
  116. const destination = path.join(output, 'schemas', name);
  117. // Remove the existing directory if necessary.
  118. if (fs.existsSync(destination)) {
  119. try {
  120. const oldPackagePath = path.join(destination, 'package.json.orig');
  121. const oldPackageData = utils.readJSONFile(oldPackagePath);
  122. if (oldPackageData.version === data.version) {
  123. fs.removeSync(destination);
  124. }
  125. } catch (e) {
  126. fs.removeSync(destination);
  127. }
  128. }
  129. // Make sure the schema directory exists.
  130. fs.mkdirpSync(destination);
  131. // Copy schemas.
  132. schemas.forEach(schema => {
  133. const file = path.basename(schema);
  134. fs.copySync(schema, path.join(destination, file));
  135. });
  136. // Write the package.json file for future comparison.
  137. fs.copySync(
  138. path.join(packageDir, 'package.json'),
  139. path.join(destination, 'package.json.orig')
  140. );
  141. }
  142. if (!themePath) {
  143. return;
  144. }
  145. themeConfig.push({
  146. mode: 'production',
  147. entry: {
  148. index: path.join(name, themePath)
  149. },
  150. output: {
  151. path: path.resolve(path.join(output, 'themes', name)),
  152. // we won't use these JS files, only the extracted CSS
  153. filename: '[name].js'
  154. },
  155. module: {
  156. rules: [
  157. {
  158. test: /\.css$/,
  159. use: [MiniCssExtractPlugin.loader, 'css-loader']
  160. },
  161. {
  162. test: /\.svg/,
  163. use: [{ loader: 'svg-url-loader', options: { encoding: 'none' } }]
  164. },
  165. {
  166. test: /\.(cur|png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
  167. use: [{ loader: 'url-loader', options: { limit: 10000 } }]
  168. }
  169. ]
  170. },
  171. plugins: [
  172. new MiniCssExtractPlugin({
  173. // Options similar to the same options in webpackOptions.output
  174. // both options are optional
  175. filename: '[name].css',
  176. chunkFilename: '[id].css'
  177. })
  178. ]
  179. });
  180. });
  181. // Template the CSS index file.
  182. cssImports = cssImports.sort((a, b) => a.localeCompare(b));
  183. let cssContents = '/* This is a generated file of CSS imports */';
  184. cssContents +=
  185. '\n/* It was generated by @jupyterlab/buildutils in Build.ensureAssets() */';
  186. cssContents += `\n@import url('~${appCSS}');`;
  187. cssImports.forEach(cssImport => {
  188. cssContents += `\n@import url('~${cssImport}');`;
  189. });
  190. cssContents += '\n';
  191. const indexCSSPath = path.join(output, 'imports.css');
  192. // Make sure the output dir exists before writing to it.
  193. if (!fs.existsSync(output)) {
  194. fs.mkdirSync(output);
  195. }
  196. fs.writeFileSync(indexCSSPath, cssContents, { encoding: 'utf8' });
  197. return themeConfig;
  198. }
  199. /**
  200. * Returns JupyterLab extension metadata from a module.
  201. */
  202. export function normalizeExtension(module: IModule): ILabExtension {
  203. let { jupyterlab, main, name } = module;
  204. main = main || 'index.js';
  205. if (!jupyterlab) {
  206. throw new Error(`Module ${name} does not contain JupyterLab metadata.`);
  207. }
  208. let { extension, mimeExtension, schemaDir, themePath } = jupyterlab;
  209. extension = extension === true ? main : extension;
  210. mimeExtension = mimeExtension === true ? main : mimeExtension;
  211. if (extension && mimeExtension && extension === mimeExtension) {
  212. const message = 'extension and mimeExtension cannot be the same export.';
  213. throw new Error(message);
  214. }
  215. return { extension, mimeExtension, schemaDir, themePath };
  216. }
  217. }