build.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. let cssImports: string[] = [];
  110. packageNames.forEach(name => {
  111. packagePaths.push(
  112. path.dirname(require.resolve(path.join(name, 'package.json')))
  113. );
  114. });
  115. packagePaths.forEach(packagePath => {
  116. const packageDataPath = require.resolve(
  117. path.join(packagePath, 'package.json')
  118. );
  119. const packageDir = path.dirname(packageDataPath);
  120. const data = readJSONFile(packageDataPath);
  121. const name = data.name;
  122. const extension = normalizeExtension(data);
  123. const { schemaDir, themePath } = extension;
  124. // We prefer the styleModule key if it exists, falling back to
  125. // the normal style key.
  126. if (typeof data.styleModule === 'string') {
  127. cssImports.push(`${name}/${data.styleModule}`);
  128. } else if (typeof data.style === 'string') {
  129. cssImports.push(`${name}/${data.style}`);
  130. }
  131. // Handle schemas.
  132. if (schemaDir) {
  133. const schemas = glob.sync(
  134. path.join(path.join(packageDir, schemaDir), '*')
  135. );
  136. const destination = path.join(schemaOutput, 'schemas', name);
  137. // Remove the existing directory if necessary.
  138. if (fs.existsSync(destination)) {
  139. try {
  140. const oldPackagePath = path.join(destination, 'package.json.orig');
  141. const oldPackageData = readJSONFile(oldPackagePath);
  142. if (oldPackageData.version === data.version) {
  143. fs.removeSync(destination);
  144. }
  145. } catch (e) {
  146. fs.removeSync(destination);
  147. }
  148. }
  149. // Make sure the schema directory exists.
  150. fs.mkdirpSync(destination);
  151. // Copy schemas.
  152. schemas.forEach(schema => {
  153. const file = path.basename(schema);
  154. fs.copySync(schema, path.join(destination, file));
  155. });
  156. // Write the package.json file for future comparison.
  157. fs.copySync(
  158. path.join(packageDir, 'package.json'),
  159. path.join(destination, 'package.json.orig')
  160. );
  161. }
  162. if (!themePath) {
  163. return;
  164. }
  165. themeConfig.push({
  166. mode: 'production',
  167. entry: {
  168. index: path.join(packageDir, themePath)
  169. },
  170. output: {
  171. path: path.resolve(path.join(themeOutput, 'themes', name)),
  172. // we won't use these JS files, only the extracted CSS
  173. filename: '[name].js',
  174. hashFunction: 'sha256'
  175. },
  176. module: {
  177. rules: [
  178. {
  179. test: /\.css$/,
  180. use: [MiniCssExtractPlugin.loader, 'css-loader']
  181. },
  182. {
  183. test: /\.svg/,
  184. use: [{ loader: 'svg-url-loader', options: { encoding: 'none' } }]
  185. },
  186. {
  187. test: /\.(cur|png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
  188. use: [{ loader: 'url-loader', options: { limit: 10000 } }]
  189. }
  190. ]
  191. },
  192. plugins: [
  193. new MiniCssExtractPlugin({
  194. // Options similar to the same options in webpackOptions.output
  195. // both options are optional
  196. filename: '[name].css',
  197. chunkFilename: '[id].css'
  198. })
  199. ]
  200. });
  201. });
  202. cssImports.sort((a, b) => a.localeCompare(b));
  203. const styleContents = `/* This is a generated file of CSS imports */
  204. /* It was generated by @jupyterlab/builder in Build.ensureAssets() */
  205. ${cssImports.map(x => `import '${x}';`).join('\n')}
  206. `;
  207. const stylePath = path.join(output, 'style.js');
  208. // Make sure the output dir exists before writing to it.
  209. if (!fs.existsSync(output)) {
  210. fs.mkdirSync(output);
  211. }
  212. fs.writeFileSync(stylePath, styleContents, {
  213. encoding: 'utf8'
  214. });
  215. return themeConfig;
  216. }
  217. /**
  218. * Returns JupyterLab extension metadata from a module.
  219. */
  220. export function normalizeExtension(module: IModule): ILabExtension {
  221. let { jupyterlab, main, name } = module;
  222. main = main || 'index.js';
  223. if (!jupyterlab) {
  224. throw new Error(`Module ${name} does not contain JupyterLab metadata.`);
  225. }
  226. let { extension, mimeExtension, schemaDir, themePath } = jupyterlab;
  227. extension = extension === true ? main : extension;
  228. mimeExtension = mimeExtension === true ? main : mimeExtension;
  229. if (extension && mimeExtension && extension === mimeExtension) {
  230. const message = 'extension and mimeExtension cannot be the same export.';
  231. throw new Error(message);
  232. }
  233. return { extension, mimeExtension, schemaDir, themePath };
  234. }
  235. }