1 |
|
2 |
|
3 |
|
4 | import * as path from 'path';
|
5 | import * as webpack from 'webpack';
|
6 | import { Build } from './build';
|
7 | import { WPPlugin } from './webpack-plugins';
|
8 | import { merge } from 'webpack-merge';
|
9 | import * as fs from 'fs-extra';
|
10 | import * as glob from 'glob';
|
11 | import Ajv from 'ajv';
|
12 |
|
13 | const baseConfig = require('./webpack.config.base');
|
14 | const { ModuleFederationPlugin } = webpack.container;
|
15 |
|
16 | export interface IOptions {
|
17 | packagePath?: string;
|
18 | corePath?: string;
|
19 | staticUrl?: string;
|
20 | mode?: 'development' | 'production';
|
21 | devtool?: string;
|
22 | watchMode?: boolean;
|
23 | }
|
24 |
|
25 | function generateConfig({
|
26 | packagePath = '',
|
27 | corePath = '',
|
28 | staticUrl = '',
|
29 | mode = 'production',
|
30 | devtool = mode === 'development' ? 'source-map' : undefined,
|
31 | watchMode = false
|
32 | }: IOptions = {}): webpack.Configuration[] {
|
33 | const data = require(path.join(packagePath, 'package.json'));
|
34 |
|
35 | const ajv = new Ajv({ useDefaults: true, strict: false });
|
36 | const validate = ajv.compile(require('../metadata_schema.json'));
|
37 | let valid = validate(data.jupyterlab ?? {});
|
38 | if (!valid) {
|
39 | console.error(validate.errors);
|
40 | process.exit(1);
|
41 | }
|
42 |
|
43 | const outputPath = path.join(packagePath, data.jupyterlab['outputDir']);
|
44 | const staticPath = path.join(outputPath, 'static');
|
45 |
|
46 |
|
47 | const index = require.resolve(packagePath);
|
48 | const exposes: { [id: string]: string } = {
|
49 | './index': index
|
50 | };
|
51 |
|
52 | const extension = data.jupyterlab.extension;
|
53 | if (extension === true) {
|
54 | exposes['./extension'] = index;
|
55 | } else if (typeof extension === 'string') {
|
56 | exposes['./extension'] = path.join(packagePath, extension);
|
57 | }
|
58 |
|
59 | const mimeExtension = data.jupyterlab.mimeExtension;
|
60 | if (mimeExtension === true) {
|
61 | exposes['./mimeExtension'] = index;
|
62 | } else if (typeof mimeExtension === 'string') {
|
63 | exposes['./mimeExtension'] = path.join(packagePath, mimeExtension);
|
64 | }
|
65 |
|
66 | if (typeof data.styleModule === 'string') {
|
67 | exposes['./style'] = path.join(packagePath, data.styleModule);
|
68 | } else if (typeof data.style === 'string') {
|
69 | exposes['./style'] = path.join(packagePath, data.style);
|
70 | }
|
71 |
|
72 | const coreData = require(path.join(corePath, 'package.json'));
|
73 |
|
74 | let shared: any = {};
|
75 |
|
76 |
|
77 | const coreDeps: any = {
|
78 | ...coreData.dependencies,
|
79 | ...(coreData.resolutions ?? {})
|
80 | };
|
81 |
|
82 |
|
83 |
|
84 | Object.keys(coreDeps).forEach(element => {
|
85 | shared[element] = {
|
86 | requiredVersion: coreDeps[element].replace('~', '^'),
|
87 | import: false
|
88 | };
|
89 | });
|
90 |
|
91 |
|
92 | Object.keys(data.dependencies).forEach(element => {
|
93 |
|
94 |
|
95 | if (!shared[element]) {
|
96 | shared[element] = {};
|
97 | }
|
98 | });
|
99 |
|
100 |
|
101 | coreData.jupyterlab.singletonPackages.forEach((element: string) => {
|
102 | if (!shared[element]) {
|
103 | shared[element] = {};
|
104 | }
|
105 | shared[element].import = false;
|
106 | shared[element].singleton = true;
|
107 | });
|
108 |
|
109 |
|
110 |
|
111 | const sharedPackages = data.jupyterlab.sharedPackages ?? {};
|
112 |
|
113 |
|
114 | Object.keys(sharedPackages).forEach(pkg => {
|
115 | if (sharedPackages[pkg] === false) {
|
116 | delete shared[pkg];
|
117 | delete sharedPackages[pkg];
|
118 | }
|
119 | });
|
120 |
|
121 |
|
122 | Object.keys(sharedPackages).forEach(pkg => {
|
123 |
|
124 | if (sharedPackages[pkg].bundled === false) {
|
125 | sharedPackages[pkg].import = false;
|
126 | } else if (
|
127 | sharedPackages[pkg].bundled === true &&
|
128 | shared[pkg]?.import === false
|
129 | ) {
|
130 |
|
131 | delete shared[pkg].import;
|
132 | }
|
133 | delete sharedPackages[pkg].bundled;
|
134 | });
|
135 |
|
136 | shared = merge(shared, sharedPackages);
|
137 |
|
138 |
|
139 | if (shared[data.name]) {
|
140 | console.error(
|
141 | `The root package itself '${data.name}' may not specified as a shared dependency.`
|
142 | );
|
143 | }
|
144 | shared[data.name] = {
|
145 | version: data.version,
|
146 | singleton: true,
|
147 | import: index
|
148 | };
|
149 |
|
150 |
|
151 |
|
152 | fs.emptyDirSync(outputPath);
|
153 |
|
154 | const extras = Build.ensureAssets({
|
155 | packageNames: [],
|
156 | packagePaths: [packagePath],
|
157 | output: staticPath,
|
158 | schemaOutput: outputPath,
|
159 | themeOutput: outputPath
|
160 | });
|
161 |
|
162 | fs.copyFileSync(
|
163 | path.join(packagePath, 'package.json'),
|
164 | path.join(outputPath, 'package.json')
|
165 | );
|
166 |
|
167 | class CleanupPlugin {
|
168 | apply(compiler: any) {
|
169 | compiler.hooks.done.tap('Cleanup', (stats: any) => {
|
170 | const newlyCreatedAssets = stats.compilation.assets;
|
171 |
|
172 |
|
173 |
|
174 | const files = glob.sync(path.join(staticPath, 'remoteEntry.*.js'));
|
175 | let newEntry = '';
|
176 | const unlinked: string[] = [];
|
177 | files.forEach(file => {
|
178 | const fileName = path.basename(file);
|
179 | if (!newlyCreatedAssets[fileName]) {
|
180 | fs.unlinkSync(path.resolve(file));
|
181 | unlinked.push(fileName);
|
182 | } else {
|
183 | newEntry = fileName;
|
184 | }
|
185 | });
|
186 | if (unlinked.length > 0) {
|
187 | console.log('Removed old assets: ', unlinked);
|
188 | }
|
189 |
|
190 |
|
191 | const data = fs.readJSONSync(path.join(outputPath, 'package.json'));
|
192 | const _build: any = {
|
193 | load: path.join('static', newEntry)
|
194 | };
|
195 | if (exposes['./extension'] !== undefined) {
|
196 | _build.extension = './extension';
|
197 | }
|
198 | if (exposes['./mimeExtension'] !== undefined) {
|
199 | _build.mimeExtension = './mimeExtension';
|
200 | }
|
201 | if (exposes['./style'] !== undefined) {
|
202 | _build.style = './style';
|
203 | }
|
204 | data.jupyterlab._build = _build;
|
205 | fs.writeJSONSync(path.join(outputPath, 'package.json'), data, {
|
206 | spaces: 2
|
207 | });
|
208 | });
|
209 | }
|
210 | }
|
211 |
|
212 |
|
213 | let webpackConfigPath = data.jupyterlab['webpackConfig'];
|
214 | let webpackConfig = {};
|
215 |
|
216 |
|
217 |
|
218 | if (webpackConfigPath) {
|
219 | webpackConfigPath = path.join(packagePath, webpackConfigPath);
|
220 | if (fs.existsSync(webpackConfigPath)) {
|
221 | webpackConfig = require(webpackConfigPath);
|
222 | }
|
223 | }
|
224 |
|
225 | let plugins = [
|
226 | new ModuleFederationPlugin({
|
227 | name: data.name,
|
228 | library: {
|
229 | type: 'var',
|
230 | name: ['_JUPYTERLAB', data.name]
|
231 | },
|
232 | filename: 'remoteEntry.[contenthash].js',
|
233 | exposes,
|
234 | shared
|
235 | }),
|
236 | new CleanupPlugin()
|
237 | ];
|
238 |
|
239 | if (mode === 'production') {
|
240 | plugins.push(
|
241 | new WPPlugin.JSONLicenseWebpackPlugin({
|
242 | excludedPackageTest: packageName => packageName === data.name
|
243 | })
|
244 | );
|
245 | }
|
246 |
|
247 |
|
248 |
|
249 | let filename = '[name].[contenthash].js';
|
250 | if (mode === 'production') {
|
251 | filename += '?v=[contenthash]';
|
252 | }
|
253 |
|
254 | const rules: any = [{ test: /\.html$/, type: 'asset/resource' }];
|
255 |
|
256 | if (mode === 'development') {
|
257 | rules.push({
|
258 | test: /\.js$/,
|
259 | enforce: 'pre',
|
260 | use: ['source-map-loader']
|
261 | });
|
262 | }
|
263 |
|
264 | const config = [
|
265 | merge(
|
266 | baseConfig,
|
267 | {
|
268 | mode,
|
269 | devtool,
|
270 | entry: {},
|
271 | output: {
|
272 | filename,
|
273 | path: staticPath,
|
274 | publicPath: staticUrl || 'auto'
|
275 | },
|
276 | plugins
|
277 | },
|
278 | webpackConfig,
|
279 | {
|
280 | module: {
|
281 | rules
|
282 | }
|
283 | }
|
284 | )
|
285 | ].concat(extras);
|
286 |
|
287 | if (mode === 'development') {
|
288 | const logPath = path.join(outputPath, 'build_log.json');
|
289 | function regExpReplacer(key: any, value: any) {
|
290 | if (value instanceof RegExp) {
|
291 | return value.toString();
|
292 | } else {
|
293 | return value;
|
294 | }
|
295 | }
|
296 | fs.writeFileSync(logPath, JSON.stringify(config, regExpReplacer, ' '));
|
297 | }
|
298 | return config;
|
299 | }
|
300 |
|
301 | export default generateConfig;
|