build.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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 { readJSONFile } from '@jupyterlab/buildutils';
  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 directory for the schema directory, defaults to the output directory.
  26. */
  27. schemaOutput?: string;
  28. /**
  29. * The directory for the theme directory, defaults to the output directory
  30. */
  31. themeOutput?: string;
  32. /**
  33. * The names of the packages to ensure.
  34. */
  35. packageNames: ReadonlyArray<string>;
  36. /**
  37. * The package paths to ensure.
  38. */
  39. packagePaths?: ReadonlyArray<string>;
  40. }
  41. /**
  42. * The JupyterLab extension attributes in a module.
  43. */
  44. export interface ILabExtension {
  45. /**
  46. * Indicates whether the extension is a standalone extension.
  47. *
  48. * #### Notes
  49. * If `true`, the `main` export of the package is used. If set to a string
  50. * path, the export from that path is loaded as a JupyterLab extension. It
  51. * is possible for one package to have both an `extension` and a
  52. * `mimeExtension` but they cannot be identical (i.e., the same export
  53. * cannot be declared both an `extension` and a `mimeExtension`).
  54. */
  55. readonly extension?: boolean | string;
  56. /**
  57. * Indicates whether the extension is a MIME renderer extension.
  58. *
  59. * #### Notes
  60. * If `true`, the `main` export of the package is used. If set to a string
  61. * path, the export from that path is loaded as a JupyterLab extension. It
  62. * is possible for one package to have both an `extension` and a
  63. * `mimeExtension` but they cannot be identical (i.e., the same export
  64. * cannot be declared both an `extension` and a `mimeExtension`).
  65. */
  66. readonly mimeExtension?: boolean | string;
  67. /**
  68. * The local schema file path in the extension package.
  69. */
  70. readonly schemaDir?: string;
  71. /**
  72. * The local theme file path in the extension package.
  73. */
  74. readonly themePath?: string;
  75. }
  76. /**
  77. * A minimal definition of a module's package definition (i.e., package.json).
  78. */
  79. export interface IModule {
  80. /**
  81. * The JupyterLab metadata/
  82. */
  83. jupyterlab?: ILabExtension;
  84. /**
  85. * The main entry point in a module.
  86. */
  87. main?: string;
  88. /**
  89. * The name of a module.
  90. */
  91. name: string;
  92. }
  93. /**
  94. * Ensures that the assets of plugin packages are populated for a build.
  95. *
  96. * @ Returns An array of lab extension config data.
  97. */
  98. export function ensureAssets(
  99. options: IEnsureOptions
  100. ): webpack.Configuration[] {
  101. const {
  102. output,
  103. schemaOutput = output,
  104. themeOutput = output,
  105. packageNames
  106. } = options;
  107. const themeConfig: webpack.Configuration[] = [];
  108. const packagePaths: string[] = options.packagePaths?.slice() || [];
  109. // Get the CSS imports.
  110. // We must import the application CSS first.
  111. // The order of the rest does not matter.
  112. // We explicitly ignore themes so they can be loaded dynamically.
  113. let cssImports: Array<string> = [];
  114. let appCSS = '';
  115. packageNames.forEach(name => {
  116. packagePaths.push(
  117. path.dirname(require.resolve(path.join(name, 'package.json')))
  118. );
  119. });
  120. packagePaths.forEach(packagePath => {
  121. const packageDataPath = require.resolve(
  122. path.join(packagePath, 'package.json')
  123. );
  124. const packageDir = path.dirname(packageDataPath);
  125. const data = readJSONFile(packageDataPath);
  126. const name = data.name;
  127. const extension = normalizeExtension(data);
  128. const { schemaDir, themePath } = extension;
  129. // Handle styles.
  130. if (data.style) {
  131. if (data.name === '@jupyterlab/application-extension') {
  132. appCSS = name + '/' + data.style;
  133. } else if (!data.jupyterlab.themePath) {
  134. cssImports.push(name + '/' + data.style);
  135. }
  136. }
  137. // Handle schemas.
  138. if (schemaDir) {
  139. const schemas = glob.sync(
  140. path.join(path.join(packageDir, schemaDir), '*')
  141. );
  142. const destination = path.join(schemaOutput, 'schemas', name);
  143. // Remove the existing directory if necessary.
  144. if (fs.existsSync(destination)) {
  145. try {
  146. const oldPackagePath = path.join(destination, 'package.json.orig');
  147. const oldPackageData = readJSONFile(oldPackagePath);
  148. if (oldPackageData.version === data.version) {
  149. fs.removeSync(destination);
  150. }
  151. } catch (e) {
  152. fs.removeSync(destination);
  153. }
  154. }
  155. // Make sure the schema directory exists.
  156. fs.mkdirpSync(destination);
  157. // Copy schemas.
  158. schemas.forEach(schema => {
  159. const file = path.basename(schema);
  160. fs.copySync(schema, path.join(destination, file));
  161. });
  162. // Write the package.json file for future comparison.
  163. fs.copySync(
  164. path.join(packageDir, 'package.json'),
  165. path.join(destination, 'package.json.orig')
  166. );
  167. }
  168. if (!themePath) {
  169. return;
  170. }
  171. themeConfig.push({
  172. mode: 'production',
  173. entry: {
  174. index: path.join(packageDir, themePath)
  175. },
  176. output: {
  177. path: path.resolve(path.join(themeOutput, 'themes', name)),
  178. // we won't use these JS files, only the extracted CSS
  179. filename: '[name].js'
  180. },
  181. module: {
  182. rules: [
  183. {
  184. test: /\.css$/,
  185. use: [MiniCssExtractPlugin.loader, 'css-loader']
  186. },
  187. {
  188. test: /\.svg/,
  189. use: [{ loader: 'svg-url-loader', options: { encoding: 'none' } }]
  190. },
  191. {
  192. test: /\.(cur|png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
  193. use: [{ loader: 'url-loader', options: { limit: 10000 } }]
  194. }
  195. ]
  196. },
  197. plugins: [
  198. new MiniCssExtractPlugin({
  199. // Options similar to the same options in webpackOptions.output
  200. // both options are optional
  201. filename: '[name].css',
  202. chunkFilename: '[id].css'
  203. })
  204. ]
  205. });
  206. });
  207. // Template the CSS index file.
  208. cssImports = cssImports.sort((a, b) => a.localeCompare(b));
  209. let cssContents = '/* This is a generated file of CSS imports */';
  210. cssContents +=
  211. '\n/* It was generated by @jupyterlab/builder in Build.ensureAssets() */';
  212. cssContents += `\n@import url('~${appCSS}');`;
  213. cssImports.forEach(cssImport => {
  214. cssContents += `\n@import url('~${cssImport}');`;
  215. });
  216. cssContents += '\n';
  217. const indexCSSPath = path.join(output, 'imports.css');
  218. // Make sure the output dir exists before writing to it.
  219. if (!fs.existsSync(output)) {
  220. fs.mkdirSync(output);
  221. }
  222. fs.writeFileSync(indexCSSPath, cssContents, { encoding: 'utf8' });
  223. return themeConfig;
  224. }
  225. /**
  226. * Returns JupyterLab extension metadata from a module.
  227. */
  228. export function normalizeExtension(module: IModule): ILabExtension {
  229. let { jupyterlab, main, name } = module;
  230. main = main || 'index.js';
  231. if (!jupyterlab) {
  232. throw new Error(`Module ${name} does not contain JupyterLab metadata.`);
  233. }
  234. let { extension, mimeExtension, schemaDir, themePath } = jupyterlab;
  235. extension = extension === true ? main : extension;
  236. mimeExtension = mimeExtension === true ? main : mimeExtension;
  237. if (extension && mimeExtension && extension === mimeExtension) {
  238. const message = 'extension and mimeExtension cannot be the same export.';
  239. throw new Error(message);
  240. }
  241. return { extension, mimeExtension, schemaDir, themePath };
  242. }
  243. }