UNPKG

8.12 kBPlain TextView Raw
1/* -----------------------------------------------------------------------------
2| Copyright (c) Jupyter Development Team.
3| Distributed under the terms of the Modified BSD License.
4|----------------------------------------------------------------------------*/
5
6import MiniCssExtractPlugin from 'mini-css-extract-plugin';
7import miniSVGDataURI from 'mini-svg-data-uri';
8
9import * as webpack from 'webpack';
10import * as fs from 'fs-extra';
11import * as glob from 'glob';
12import * as path from 'path';
13
14/**
15 * A namespace for JupyterLab build utilities.
16 */
17export namespace Build {
18 /**
19 * The options used to ensure a root package has the appropriate
20 * assets for its JupyterLab extension packages.
21 */
22 export interface IEnsureOptions {
23 /**
24 * The output directory where the build assets should reside.
25 */
26 output: string;
27
28 /**
29 * The directory for the schema directory, defaults to the output directory.
30 */
31 schemaOutput?: string;
32
33 /**
34 * The directory for the theme directory, defaults to the output directory
35 */
36 themeOutput?: string;
37
38 /**
39 * The names of the packages to ensure.
40 */
41 packageNames: ReadonlyArray<string>;
42
43 /**
44 * The package paths to ensure.
45 */
46 packagePaths?: ReadonlyArray<string>;
47 }
48
49 /**
50 * The JupyterLab extension attributes in a module.
51 */
52 export interface ILabExtension {
53 /**
54 * Indicates whether the extension is a standalone extension.
55 *
56 * #### Notes
57 * If `true`, the `main` export of the package is used. If set to a string
58 * path, the export from that path is loaded as a JupyterLab extension. It
59 * is possible for one package to have both an `extension` and a
60 * `mimeExtension` but they cannot be identical (i.e., the same export
61 * cannot be declared both an `extension` and a `mimeExtension`).
62 */
63 readonly extension?: boolean | string;
64
65 /**
66 * Indicates whether the extension is a MIME renderer extension.
67 *
68 * #### Notes
69 * If `true`, the `main` export of the package is used. If set to a string
70 * path, the export from that path is loaded as a JupyterLab extension. It
71 * is possible for one package to have both an `extension` and a
72 * `mimeExtension` but they cannot be identical (i.e., the same export
73 * cannot be declared both an `extension` and a `mimeExtension`).
74 */
75 readonly mimeExtension?: boolean | string;
76
77 /**
78 * The local schema file path in the extension package.
79 */
80 readonly schemaDir?: string;
81
82 /**
83 * The local theme file path in the extension package.
84 */
85 readonly themePath?: string;
86 }
87
88 /**
89 * A minimal definition of a module's package definition (i.e., package.json).
90 */
91 export interface IModule {
92 /**
93 * The JupyterLab metadata/
94 */
95 jupyterlab?: ILabExtension;
96
97 /**
98 * The main entry point in a module.
99 */
100 main?: string;
101
102 /**
103 * The name of a module.
104 */
105 name: string;
106 }
107
108 /**
109 * Ensures that the assets of plugin packages are populated for a build.
110 *
111 * @ Returns An array of lab extension config data.
112 */
113 export function ensureAssets(
114 options: IEnsureOptions
115 ): webpack.Configuration[] {
116 const {
117 output,
118 schemaOutput = output,
119 themeOutput = output,
120 packageNames
121 } = options;
122
123 const themeConfig: webpack.Configuration[] = [];
124
125 const packagePaths: string[] = options.packagePaths?.slice() || [];
126
127 let cssImports: string[] = [];
128
129 packageNames.forEach(name => {
130 packagePaths.push(
131 path.dirname(require.resolve(path.join(name, 'package.json')))
132 );
133 });
134
135 packagePaths.forEach(packagePath => {
136 const packageDataPath = require.resolve(
137 path.join(packagePath, 'package.json')
138 );
139 const packageDir = path.dirname(packageDataPath);
140 const data = fs.readJSONSync(packageDataPath);
141 const name = data.name;
142 const extension = normalizeExtension(data);
143
144 const { schemaDir, themePath } = extension;
145
146 // We prefer the styleModule key if it exists, falling back to
147 // the normal style key.
148 if (typeof data.styleModule === 'string') {
149 cssImports.push(`${name}/${data.styleModule}`);
150 } else if (typeof data.style === 'string') {
151 cssImports.push(`${name}/${data.style}`);
152 }
153
154 // Handle schemas.
155 if (schemaDir) {
156 const schemas = glob.sync(
157 path.join(path.join(packageDir, schemaDir), '*')
158 );
159 const destination = path.join(schemaOutput, 'schemas', name);
160
161 // Remove the existing directory if necessary.
162 if (fs.existsSync(destination)) {
163 try {
164 const oldPackagePath = path.join(destination, 'package.json.orig');
165 const oldPackageData = fs.readJSONSync(oldPackagePath);
166 if (oldPackageData.version === data.version) {
167 fs.removeSync(destination);
168 }
169 } catch (e) {
170 fs.removeSync(destination);
171 }
172 }
173
174 // Make sure the schema directory exists.
175 fs.mkdirpSync(destination);
176
177 // Copy schemas.
178 schemas.forEach(schema => {
179 const file = path.basename(schema);
180 fs.copySync(schema, path.join(destination, file));
181 });
182
183 // Write the package.json file for future comparison.
184 fs.copySync(
185 path.join(packageDir, 'package.json'),
186 path.join(destination, 'package.json.orig')
187 );
188 }
189
190 if (!themePath) {
191 return;
192 }
193 themeConfig.push({
194 mode: 'production',
195 entry: {
196 index: path.join(packageDir, themePath)
197 },
198 output: {
199 path: path.resolve(path.join(themeOutput, 'themes', name)),
200 // we won't use these JS files, only the extracted CSS
201 filename: '[name].js',
202 hashFunction: 'sha256'
203 },
204 module: {
205 rules: [
206 {
207 test: /\.css$/,
208 use: [MiniCssExtractPlugin.loader, 'css-loader']
209 },
210 {
211 test: /\.svg/,
212 type: 'asset/inline',
213 generator: {
214 dataUrl: (content: any) => miniSVGDataURI(content.toString())
215 }
216 },
217 {
218 test: /\.(cur|png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
219 type: 'asset'
220 }
221 ]
222 },
223 plugins: [
224 new MiniCssExtractPlugin({
225 // Options similar to the same options in webpackOptions.output
226 // both options are optional
227 filename: '[name].css',
228 chunkFilename: '[id].css'
229 })
230 ]
231 });
232 });
233
234 cssImports.sort((a, b) => a.localeCompare(b));
235 const styleContents = `/* This is a generated file of CSS imports */
236/* It was generated by @jupyterlab/builder in Build.ensureAssets() */
237
238${cssImports.map(x => `import '${x}';`).join('\n')}
239`;
240
241 const stylePath = path.join(output, 'style.js');
242
243 // Make sure the output dir exists before writing to it.
244 if (!fs.existsSync(output)) {
245 fs.mkdirSync(output);
246 }
247 fs.writeFileSync(stylePath, styleContents, {
248 encoding: 'utf8'
249 });
250
251 return themeConfig;
252 }
253
254 /**
255 * Returns JupyterLab extension metadata from a module.
256 */
257 export function normalizeExtension(module: IModule): ILabExtension {
258 let { jupyterlab, main, name } = module;
259
260 main = main || 'index.js';
261
262 if (!jupyterlab) {
263 throw new Error(`Module ${name} does not contain JupyterLab metadata.`);
264 }
265
266 let { extension, mimeExtension, schemaDir, themePath } = jupyterlab;
267
268 extension = extension === true ? main : extension;
269 mimeExtension = mimeExtension === true ? main : mimeExtension;
270
271 if (extension && mimeExtension && extension === mimeExtension) {
272 const message = 'extension and mimeExtension cannot be the same export.';
273
274 throw new Error(message);
275 }
276
277 return { extension, mimeExtension, schemaDir, themePath };
278 }
279}
280
\No newline at end of file