@gravity-ui/app-builder 0.13.0 → 0.14.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,9 +40,6 @@ const config_1 = require("../../common/webpack/config");
40
40
  async function watchClientCompilation(config, onManifestReady) {
41
41
  const clientCompilation = await buildWebpackServer(config);
42
42
  const compiler = clientCompilation.compiler;
43
- if ('compilers' in compiler) {
44
- throw new Error('Unexpected multi compiler');
45
- }
46
43
  subscribeToManifestReadyEvent(compiler, onManifestReady);
47
44
  return clientCompilation;
48
45
  }
@@ -50,7 +47,14 @@ async function buildWebpackServer(config) {
50
47
  const logger = new logger_1.Logger('webpack', config.verbose);
51
48
  const { webSocketPath = path.normalize(`/${config.client.publicPathPrefix}/build/sockjs-node`), writeToDisk, ...devServer } = config.client.devServer || {};
52
49
  const normalizedConfig = { ...config.client, devServer: { ...devServer, webSocketPath } };
53
- const webpackConfig = await (0, config_1.webpackConfigFactory)("development" /* WebpackMode.Dev */, normalizedConfig, { logger });
50
+ const webpackConfigs = [
51
+ await (0, config_1.webpackConfigFactory)("development" /* WebpackMode.Dev */, normalizedConfig, { logger }),
52
+ ];
53
+ const isSsr = Boolean(normalizedConfig.ssr);
54
+ if (isSsr) {
55
+ const logger = new logger_1.Logger('webpack(SSR)', config.verbose);
56
+ webpackConfigs.push(await (0, config_1.webpackConfigFactory)("development" /* WebpackMode.Dev */, normalizedConfig, { logger, isSsr }));
57
+ }
54
58
  const publicPath = path.normalize(config.client.publicPathPrefix + '/build/');
55
59
  const staticFolder = path.resolve(paths_1.default.appDist, 'public');
56
60
  const options = {
@@ -58,7 +62,18 @@ async function buildWebpackServer(config) {
58
62
  devMiddleware: {
59
63
  publicPath,
60
64
  stats: 'errors-warnings',
61
- writeToDisk,
65
+ writeToDisk: (target) => {
66
+ if (writeToDisk === true) {
67
+ return true;
68
+ }
69
+ if (isSsr && target.startsWith(paths_1.default.appSsrBuild)) {
70
+ return true;
71
+ }
72
+ if (typeof writeToDisk === 'function') {
73
+ return writeToDisk(target);
74
+ }
75
+ return false;
76
+ },
62
77
  },
63
78
  liveReload: false,
64
79
  hot: true,
@@ -116,7 +131,7 @@ async function buildWebpackServer(config) {
116
131
  });
117
132
  }
118
133
  options.proxy = proxy;
119
- const compiler = (0, webpack_1.default)(webpackConfig);
134
+ const compiler = (0, webpack_1.default)(webpackConfigs);
120
135
  const server = new webpack_dev_server_1.default(options, compiler);
121
136
  try {
122
137
  await server.start();
@@ -129,17 +144,28 @@ async function buildWebpackServer(config) {
129
144
  }
130
145
  return server;
131
146
  }
132
- function subscribeToManifestReadyEvent(compiler, onManifestReady) {
147
+ function subscribeToManifestReadyEvent(webpackCompiler, onManifestReady) {
133
148
  const promises = [];
134
- const assetsManifestPlugin = compiler.options.plugins.find((plugin) => plugin instanceof webpack_assets_manifest_1.default);
135
- if (assetsManifestPlugin) {
136
- const assetsManifestReady = (0, utils_1.deferredPromise)();
137
- promises.push(assetsManifestReady.promise);
138
- assetsManifestPlugin.hooks.done.tap('app-builder', assetsManifestReady.resolve);
149
+ const options = Array.isArray(webpackCompiler.options)
150
+ ? webpackCompiler.options
151
+ : [webpackCompiler.options];
152
+ const compilers = 'compilers' in webpackCompiler ? webpackCompiler.compilers : [webpackCompiler];
153
+ for (let i = 0; i < options.length; i++) {
154
+ const config = options[i];
155
+ const compiler = compilers[i];
156
+ if (!config || !compiler) {
157
+ throw new Error('Something goes wrong!');
158
+ }
159
+ const assetsManifestPlugin = config.plugins.find((plugin) => plugin instanceof webpack_assets_manifest_1.default);
160
+ if (assetsManifestPlugin) {
161
+ const assetsManifestReady = (0, utils_1.deferredPromise)();
162
+ promises.push(assetsManifestReady.promise);
163
+ assetsManifestPlugin.hooks.done.tap('app-builder', assetsManifestReady.resolve);
164
+ }
165
+ const manifestReady = (0, utils_1.deferredPromise)();
166
+ promises.push(manifestReady.promise);
167
+ const { afterEmit } = (0, webpack_manifest_plugin_1.getCompilerHooks)(compiler);
168
+ afterEmit.tap('app-builder', manifestReady.resolve);
139
169
  }
140
- const manifestReady = (0, utils_1.deferredPromise)();
141
- promises.push(manifestReady.promise);
142
- const { afterEmit } = (0, webpack_manifest_plugin_1.getCompilerHooks)(compiler);
143
- afterEmit.tap('app-builder', manifestReady.resolve);
144
170
  Promise.all(promises).then(() => onManifestReady());
145
171
  }
@@ -56,7 +56,7 @@ async function default_1(config) {
56
56
  script: `${serverPath}/index.js`,
57
57
  args: ['--dev', config.server.port ? `--port=${config.server.port}` : ''],
58
58
  env: {
59
- ...(config.server.port ? { APP_PORT: config.server.port } : undefined),
59
+ ...(config.server.port ? { APP_PORT: `${config.server.port}` } : undefined),
60
60
  },
61
61
  nodeArgs: inspect || inspectBrk
62
62
  ? [`--${inspect ? 'inspect' : 'inspect-brk'}=:::${inspect || inspectBrk}`]
@@ -191,6 +191,10 @@ export interface ClientConfig {
191
191
  * Modify or return a custom [Terser options](https://github.com/terser/terser#minify-options).
192
192
  */
193
193
  terser?: (options: TerserOptions) => TerserOptions;
194
+ ssr?: {
195
+ noExternal?: string | RegExp | (string | RegExp)[] | true;
196
+ moduleType?: 'commonjs' | 'esm';
197
+ };
194
198
  }
195
199
  export interface CdnUploadConfig {
196
200
  bucket: string;
@@ -7,6 +7,8 @@ declare const _default: {
7
7
  appDist: string;
8
8
  appRun: string;
9
9
  appBuild: string;
10
+ appSsrEntry: string;
11
+ appSsrBuild: string;
10
12
  src: string;
11
13
  libBuild: string;
12
14
  libBuildEsm: string;
@@ -36,6 +36,8 @@ exports.default = {
36
36
  appDist: resolveApp('dist'),
37
37
  appRun: resolveApp('dist/run'),
38
38
  appBuild: resolveApp('dist/public/build'),
39
+ appSsrEntry: resolveApp('src/ui/ssr'),
40
+ appSsrBuild: resolveApp('dist/ssr'),
39
41
  src: resolveApp('src'),
40
42
  libBuild: resolveApp('build'),
41
43
  libBuildEsm: resolveApp('build/esm'),
@@ -10,10 +10,15 @@ const config_1 = require("./config");
10
10
  const utils_1 = require("./utils");
11
11
  async function webpackCompile(config) {
12
12
  const logger = new logger_1.Logger('webpack', config.verbose);
13
- const webpackConfig = await (0, config_1.webpackConfigFactory)("production" /* WebpackMode.Prod */, config, { logger });
13
+ const webpackConfigs = [await (0, config_1.webpackConfigFactory)("production" /* WebpackMode.Prod */, config, { logger })];
14
+ const isSsr = Boolean(config.ssr);
15
+ if (isSsr) {
16
+ const logger = new logger_1.Logger('webpack(SSR)', config.verbose);
17
+ webpackConfigs.push(await (0, config_1.webpackConfigFactory)("production" /* WebpackMode.Prod */, config, { logger, isSsr }));
18
+ }
14
19
  logger.verbose('Config created');
15
20
  return new Promise((resolve) => {
16
- const compiler = (0, webpack_1.default)(webpackConfig, (0, utils_1.webpackCompilerHandlerFactory)(logger, async () => {
21
+ const compiler = (0, webpack_1.default)(webpackConfigs, (0, utils_1.webpackCompilerHandlerFactory)(logger, async () => {
17
22
  resolve();
18
23
  }));
19
24
  process.on('SIGINT', async () => {
@@ -7,16 +7,20 @@ export interface HelperOptions {
7
7
  isEnvDevelopment: boolean;
8
8
  isEnvProduction: boolean;
9
9
  configType: `${WebpackMode}`;
10
+ buildDirectory: string;
11
+ entriesDirectory: string;
12
+ isSsr: boolean;
10
13
  }
11
14
  export declare const enum WebpackMode {
12
15
  Prod = "production",
13
16
  Dev = "development"
14
17
  }
15
- export declare function webpackConfigFactory(webpackMode: WebpackMode, config: NormalizedClientConfig, { logger }?: {
18
+ export declare function webpackConfigFactory(webpackMode: WebpackMode, config: NormalizedClientConfig, { logger, isSsr }?: {
16
19
  logger?: Logger;
20
+ isSsr?: boolean;
17
21
  }): Promise<webpack.Configuration>;
18
22
  export declare function configureModuleRules(helperOptions: HelperOptions, additionalRules?: NonNullable<webpack.RuleSetRule['oneOf']>): webpack.RuleSetRule[];
19
23
  export declare function configureResolve({ isEnvProduction, config }: HelperOptions): webpack.ResolveOptions;
20
24
  type Optimization = NonNullable<webpack.Configuration['optimization']>;
21
- export declare function configureOptimization({ config }: HelperOptions): Optimization;
25
+ export declare function configureOptimization({ config, isSsr }: HelperOptions): Optimization;
22
26
  export {};
@@ -44,6 +44,7 @@ const react_refresh_webpack_plugin_1 = __importDefault(require("@pmmmwh/react-re
44
44
  const moment_timezone_data_webpack_plugin_1 = __importDefault(require("moment-timezone-data-webpack-plugin"));
45
45
  const webpack_plugin_1 = __importDefault(require("@statoscope/webpack-plugin"));
46
46
  const circular_dependency_plugin_1 = __importDefault(require("circular-dependency-plugin"));
47
+ const webpack_node_externals_1 = __importDefault(require("webpack-node-externals"));
47
48
  const paths_1 = __importDefault(require("../paths"));
48
49
  const babel_1 = require("../babel");
49
50
  const progress_plugin_1 = require("./progress-plugin");
@@ -53,7 +54,7 @@ const log_config_1 = require("../logger/log-config");
53
54
  const utils_2 = require("../typescript/utils");
54
55
  const imagesSizeLimit = 2048;
55
56
  const fontSizeLimit = 8192;
56
- async function webpackConfigFactory(webpackMode, config, { logger } = {}) {
57
+ async function webpackConfigFactory(webpackMode, config, { logger, isSsr = false } = {}) {
57
58
  const isEnvDevelopment = webpackMode === "development" /* WebpackMode.Dev */;
58
59
  const isEnvProduction = webpackMode === "production" /* WebpackMode.Prod */;
59
60
  const helperOptions = {
@@ -62,11 +63,25 @@ async function webpackConfigFactory(webpackMode, config, { logger } = {}) {
62
63
  isEnvDevelopment,
63
64
  isEnvProduction,
64
65
  configType: webpackMode,
66
+ buildDirectory: isSsr ? paths_1.default.appSsrBuild : paths_1.default.appBuild,
67
+ entriesDirectory: isSsr ? paths_1.default.appSsrEntry : paths_1.default.appEntry,
68
+ isSsr,
65
69
  };
70
+ let externals = config.externals;
71
+ if (isSsr) {
72
+ externals =
73
+ config.ssr?.noExternal === true
74
+ ? undefined
75
+ : (0, webpack_node_externals_1.default)({
76
+ allowlist: config.ssr?.noExternal,
77
+ importType: config.ssr?.moduleType === 'esm' ? 'module' : 'commonjs',
78
+ });
79
+ }
66
80
  let webpackConfig = {
67
81
  mode: webpackMode,
68
82
  context: paths_1.default.app,
69
83
  bail: isEnvProduction,
84
+ target: isSsr ? 'node' : undefined,
70
85
  devtool: configureDevTool(helperOptions),
71
86
  entry: configureEntry(helperOptions),
72
87
  output: configureOutput(helperOptions),
@@ -76,7 +91,7 @@ async function webpackConfigFactory(webpackMode, config, { logger } = {}) {
76
91
  },
77
92
  plugins: configurePlugins(helperOptions),
78
93
  optimization: configureOptimization(helperOptions),
79
- externals: config.externals,
94
+ externals,
80
95
  node: config.node,
81
96
  watchOptions: configureWatchOptions(helperOptions),
82
97
  ignoreWarnings: [/Failed to parse source map/],
@@ -135,7 +150,10 @@ function configureWatchOptions({ config }) {
135
150
  delete watchOptions.watchPackages;
136
151
  return watchOptions;
137
152
  }
138
- function configureExperiments({ config, isEnvProduction, }) {
153
+ function configureExperiments({ config, isEnvProduction, isSsr, }) {
154
+ if (isSsr) {
155
+ return config.ssr?.moduleType === 'esm' ? { outputModule: true } : undefined;
156
+ }
139
157
  if (isEnvProduction) {
140
158
  return undefined;
141
159
  }
@@ -190,56 +208,69 @@ function createEntryArray(entry) {
190
208
  return [require.resolve('./public-path'), entry];
191
209
  }
192
210
  function addEntry(entry, file) {
193
- const newEntry = path.resolve(paths_1.default.appEntry, file);
194
211
  return {
195
212
  ...entry,
196
- [path.parse(file).name]: createEntryArray(newEntry),
213
+ [path.parse(file).name]: createEntryArray(file),
197
214
  };
198
215
  }
199
- function configureEntry({ config }) {
200
- let entries = fs.readdirSync(paths_1.default.appEntry).filter((file) => /\.[jt]sx?$/.test(file));
216
+ function configureEntry({ config, entriesDirectory }) {
217
+ let entries = fs.readdirSync(entriesDirectory).filter((file) => /\.[jt]sx?$/.test(file));
201
218
  if (Array.isArray(config.entryFilter) && config.entryFilter.length) {
202
219
  entries = entries.filter((entry) => config.entryFilter?.includes(entry.split('.')[0] ?? ''));
203
220
  }
204
221
  if (!entries.length) {
205
- throw new Error('No entries were found after applying UI_CORE_ENTRY_FILTER');
222
+ throw new Error('No entries were found after applying entry filter');
206
223
  }
207
- return entries.reduce((entry, file) => addEntry(entry, file), {});
224
+ return entries.reduce((entry, file) => addEntry(entry, path.resolve(entriesDirectory, file)), {});
208
225
  }
209
- function getFileNames({ isEnvProduction }) {
226
+ function getFileNames({ isEnvProduction, isSsr, config }) {
227
+ let ext = 'js';
228
+ if (isSsr) {
229
+ ext = config.ssr?.moduleType === 'esm' ? 'mjs' : 'cjs';
230
+ }
210
231
  return {
211
- filename: isEnvProduction ? 'js/[name].[contenthash:8].js' : 'js/[name].js',
232
+ filename: isEnvProduction ? `js/[name].[contenthash:8].${ext}` : `js/[name].${ext}`,
212
233
  chunkFilename: isEnvProduction
213
234
  ? 'js/[name].[contenthash:8].chunk.js'
214
235
  : 'js/[name].chunk.js',
215
236
  };
216
237
  }
217
- function configureOutput({ isEnvDevelopment, ...rest }) {
238
+ function configureOutput(options) {
239
+ let ssrOptions;
240
+ if (options.isSsr) {
241
+ ssrOptions = {
242
+ library: { type: options.config.ssr?.moduleType === 'esm' ? 'module' : 'commonjs2' },
243
+ chunkFormat: false,
244
+ };
245
+ }
218
246
  return {
219
- ...getFileNames({ isEnvDevelopment, ...rest }),
220
- path: paths_1.default.appBuild,
221
- pathinfo: isEnvDevelopment,
247
+ ...getFileNames(options),
248
+ path: options.buildDirectory,
249
+ pathinfo: options.isEnvDevelopment,
250
+ ...ssrOptions,
222
251
  };
223
252
  }
224
- function createJavaScriptLoader({ isEnvProduction, isEnvDevelopment, configType, config, }) {
253
+ function createJavaScriptLoader({ isEnvProduction, isEnvDevelopment, configType, config, isSsr, }) {
225
254
  const plugins = [];
226
- if (isEnvDevelopment && config.reactRefresh !== false) {
227
- plugins.push([
228
- require.resolve('react-refresh/babel'),
229
- config.devServer?.webSocketPath
230
- ? {
231
- overlay: {
232
- sockPath: config.devServer.webSocketPath,
233
- },
234
- }
235
- : undefined,
236
- ]);
237
- }
238
- if (isEnvProduction) {
239
- plugins.push([
240
- require.resolve('babel-plugin-import'),
241
- { libraryName: 'lodash', libraryDirectory: '', camel2DashComponentName: false },
242
- ]);
255
+ if (!isSsr) {
256
+ if (isEnvDevelopment && config.reactRefresh !== false) {
257
+ plugins.push([
258
+ require.resolve('react-refresh/babel'),
259
+ config.devServer?.webSocketPath
260
+ ? {
261
+ overlay: {
262
+ sockPath: config.devServer.webSocketPath,
263
+ },
264
+ }
265
+ : undefined,
266
+ ]);
267
+ }
268
+ if (isEnvProduction) {
269
+ plugins.push([
270
+ require.resolve('babel-plugin-import'),
271
+ { libraryName: 'lodash', libraryDirectory: '', camel2DashComponentName: false },
272
+ ]);
273
+ }
243
274
  }
244
275
  const transformOptions = config.babel({
245
276
  presets: [(0, babel_1.babelPreset)(config)],
@@ -308,22 +339,23 @@ function createWorkerRule(options) {
308
339
  };
309
340
  }
310
341
  function createSassStylesRule(options) {
311
- const loaders = getCssLoaders(options);
312
- loaders.push({
313
- loader: require.resolve('resolve-url-loader'),
314
- options: {
315
- sourceMap: !options.config.disableSourceMapGeneration,
342
+ const loaders = getCssLoaders(options, [
343
+ {
344
+ loader: require.resolve('resolve-url-loader'),
345
+ options: {
346
+ sourceMap: !options.config.disableSourceMapGeneration,
347
+ },
316
348
  },
317
- });
318
- loaders.push({
319
- loader: require.resolve('sass-loader'),
320
- options: {
321
- sourceMap: true, // must be always true for work with resolve-url-loader
322
- sassOptions: {
323
- loadPaths: [paths_1.default.appClient],
349
+ {
350
+ loader: require.resolve('sass-loader'),
351
+ options: {
352
+ sourceMap: true, // must be always true for work with resolve-url-loader
353
+ sassOptions: {
354
+ loadPaths: [paths_1.default.appClient],
355
+ },
324
356
  },
325
357
  },
326
- });
358
+ ]);
327
359
  return {
328
360
  test: /\.scss$/,
329
361
  sideEffects: options.isEnvProduction ? true : undefined,
@@ -338,29 +370,8 @@ function createStylesRule(options) {
338
370
  use: loaders,
339
371
  };
340
372
  }
341
- function getCssLoaders({ isEnvDevelopment, isEnvProduction, config }) {
373
+ function getCssLoaders({ isEnvDevelopment, isEnvProduction, config, isSsr }, additionalRules) {
342
374
  const loaders = [];
343
- if (isEnvProduction) {
344
- loaders.push(mini_css_extract_plugin_1.default.loader);
345
- }
346
- if (isEnvDevelopment) {
347
- loaders.push({
348
- loader: require.resolve('style-loader'),
349
- });
350
- }
351
- loaders.push({
352
- loader: require.resolve('css-loader'),
353
- options: {
354
- esModule: false,
355
- sourceMap: !config.disableSourceMapGeneration,
356
- importLoaders: 2,
357
- modules: {
358
- auto: true,
359
- localIdentName: '[name]__[local]--[hash:base64:5]',
360
- exportLocalsConvention: 'camelCase',
361
- },
362
- },
363
- });
364
375
  if (!config.transformCssWithLightningCss) {
365
376
  loaders.push({
366
377
  loader: require.resolve('postcss-loader'),
@@ -375,9 +386,39 @@ function getCssLoaders({ isEnvDevelopment, isEnvProduction, config }) {
375
386
  },
376
387
  });
377
388
  }
389
+ if (Array.isArray(additionalRules) && additionalRules.length > 0) {
390
+ loaders.push(...additionalRules);
391
+ }
392
+ const importLoaders = loaders.length;
393
+ loaders.unshift({
394
+ loader: require.resolve('css-loader'),
395
+ options: {
396
+ sourceMap: !config.disableSourceMapGeneration,
397
+ importLoaders,
398
+ modules: {
399
+ auto: true,
400
+ localIdentName: '[name]__[local]--[hash:base64:5]',
401
+ exportLocalsConvention: 'camelCase',
402
+ exportOnlyLocals: isSsr,
403
+ },
404
+ },
405
+ });
406
+ if (isEnvProduction) {
407
+ loaders.unshift({ loader: mini_css_extract_plugin_1.default.loader, options: { emit: !isSsr } });
408
+ }
409
+ if (isEnvDevelopment) {
410
+ if (isSsr) {
411
+ loaders.unshift({ loader: mini_css_extract_plugin_1.default.loader, options: { emit: false } });
412
+ }
413
+ else {
414
+ loaders.unshift({
415
+ loader: require.resolve('style-loader'),
416
+ });
417
+ }
418
+ }
378
419
  return loaders;
379
420
  }
380
- function createIconsRule({ isEnvProduction, config }, jsLoader) {
421
+ function createIconsRule({ isEnvProduction, config, isSsr }, jsLoader) {
381
422
  const iconIncludes = config.icons || [];
382
423
  return {
383
424
  // eslint-disable-next-line security/detect-unsafe-regex
@@ -414,11 +455,12 @@ function createIconsRule({ isEnvProduction, config }, jsLoader) {
414
455
  generator: {
415
456
  filename: 'assets/images/[name].[contenthash:8][ext]',
416
457
  publicPath: isEnvProduction ? '../' : undefined,
458
+ emit: !isSsr,
417
459
  },
418
460
  }),
419
461
  };
420
462
  }
421
- function createAssetsRules({ isEnvProduction, config }) {
463
+ function createAssetsRules({ isEnvProduction, config, isSsr }) {
422
464
  const imagesRule = {
423
465
  test: /\.(ico|bmp|gif|jpe?g|png|svg)$/,
424
466
  include: [paths_1.default.appClient, ...(config.images || [])],
@@ -430,6 +472,7 @@ function createAssetsRules({ isEnvProduction, config }) {
430
472
  },
431
473
  generator: {
432
474
  filename: 'assets/images/[name].[contenthash:8][ext]',
475
+ emit: !isSsr,
433
476
  },
434
477
  };
435
478
  const fontsRule = {
@@ -443,6 +486,7 @@ function createAssetsRules({ isEnvProduction, config }) {
443
486
  },
444
487
  generator: {
445
488
  filename: 'assets/fonts/[name].[contenthash:8][ext]',
489
+ emit: !isSsr,
446
490
  },
447
491
  };
448
492
  const rules = [imagesRule, fontsRule];
@@ -462,6 +506,7 @@ function createAssetsRules({ isEnvProduction, config }) {
462
506
  generator: {
463
507
  filename: 'assets/images/[name].[contenthash:8][ext]',
464
508
  publicPath: '../',
509
+ emit: !isSsr,
465
510
  },
466
511
  }, {
467
512
  test: /\.(ttf|eot|woff2?)$/,
@@ -476,17 +521,19 @@ function createAssetsRules({ isEnvProduction, config }) {
476
521
  generator: {
477
522
  filename: 'assets/fonts/[name].[contenthash:8][ext]',
478
523
  publicPath: '../',
524
+ emit: !isSsr,
479
525
  },
480
526
  });
481
527
  }
482
528
  return rules;
483
529
  }
484
- function createFallbackRules({ isEnvProduction }) {
530
+ function createFallbackRules({ isEnvProduction, isSsr }) {
485
531
  const rules = [
486
532
  {
487
533
  type: 'asset/resource',
488
534
  generator: {
489
535
  filename: 'assets/[name].[contenthash:8][ext]',
536
+ emit: !isSsr,
490
537
  },
491
538
  exclude: [/\.[jt]sx?$/, /\.json$/, /\.[cm]js$/, /\.ejs$/],
492
539
  },
@@ -501,6 +548,7 @@ function createFallbackRules({ isEnvProduction }) {
501
548
  generator: {
502
549
  filename: 'assets/[name].[contenthash:8][ext]',
503
550
  publicPath: '../',
551
+ emit: !isSsr,
504
552
  },
505
553
  });
506
554
  }
@@ -515,7 +563,7 @@ function createMomentTimezoneDataPlugin(options = {}) {
515
563
  return new moment_timezone_data_webpack_plugin_1.default({ ...options, startYear, endYear });
516
564
  }
517
565
  function configurePlugins(options) {
518
- const { isEnvDevelopment, isEnvProduction, config } = options;
566
+ const { isEnvDevelopment, isEnvProduction, config, isSsr } = options;
519
567
  const excludeFromClean = config.excludeFromClean || [];
520
568
  const manifestFile = 'assets-manifest.json';
521
569
  const plugins = [
@@ -539,9 +587,8 @@ function configurePlugins(options) {
539
587
  : {
540
588
  entrypoints: true,
541
589
  writeToDisk: true,
542
- output: path.resolve(paths_1.default.appBuild, manifestFile),
590
+ output: path.resolve(options.buildDirectory, manifestFile),
543
591
  }),
544
- createMomentTimezoneDataPlugin(config.momentTz),
545
592
  new webpack.DefinePlugin({
546
593
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
547
594
  ...config.definitions,
@@ -553,51 +600,12 @@ function configurePlugins(options) {
553
600
  if (process.env.WEBPACK_PROFILE === 'true') {
554
601
  plugins.push(new webpack.debug.ProfilingPlugin());
555
602
  }
556
- const contextReplacement = config.contextReplacement || {};
557
- plugins.push(new webpack.ContextReplacementPlugin(/moment[\\/]locale$/,
558
- // eslint-disable-next-line security/detect-non-literal-regexp
559
- new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})$`)));
560
- plugins.push(new webpack.ContextReplacementPlugin(/dayjs[\\/]locale$/,
561
- // eslint-disable-next-line security/detect-non-literal-regexp
562
- new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})\\.js$`)));
563
- if (contextReplacement['highlight.js']) {
564
- plugins.push(new webpack.ContextReplacementPlugin(/highlight\.js[\\/]lib[\\/]languages$/,
565
- // eslint-disable-next-line security/detect-non-literal-regexp
566
- new RegExp(`^\\./(${contextReplacement['highlight.js'].join('|')})$`)));
567
- }
568
- if (config.monaco) {
569
- const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin');
570
- plugins.push(new MonacoEditorWebpackPlugin({
571
- filename: isEnvProduction ? '[name].[hash:8].worker.js' : undefined,
572
- ...config.monaco,
573
- // currently, workers located on cdn are not working properly, so we are enforcing loading workers from
574
- // service instead
575
- publicPath: path.normalize(config.publicPathPrefix + '/build/'),
576
- }));
577
- }
578
- if (isEnvDevelopment && config.reactRefresh !== false) {
579
- const { webSocketPath = path.normalize(`/${config.publicPathPrefix}/build/sockjs-node`) } = config.devServer || {};
580
- plugins.push(new react_refresh_webpack_plugin_1.default(config.reactRefresh({
581
- overlay: { sockPath: webSocketPath },
582
- exclude: [/node_modules/, /\.worker\.[jt]sx?$/],
583
- })));
584
- }
585
- if (config.detectCircularDependencies) {
586
- let circularPluginOptions = {
587
- exclude: /node_modules/,
588
- allowAsyncCycles: true,
589
- };
590
- if (typeof config.detectCircularDependencies === 'object') {
591
- circularPluginOptions = config.detectCircularDependencies;
592
- }
593
- plugins.push(new circular_dependency_plugin_1.default(circularPluginOptions));
594
- }
595
603
  if (config.forkTsChecker !== false) {
596
604
  plugins.push(new fork_ts_checker_webpack_plugin_1.default({
597
605
  ...config.forkTsChecker,
598
606
  typescript: {
599
607
  typescriptPath: (0, utils_2.resolveTypescript)(),
600
- configFile: path.resolve(paths_1.default.app, 'src/ui/tsconfig.json'),
608
+ configFile: path.resolve(paths_1.default.appClient, 'tsconfig.json'),
601
609
  diagnosticOptions: {
602
610
  syntactic: true,
603
611
  },
@@ -606,19 +614,17 @@ function configurePlugins(options) {
606
614
  },
607
615
  }));
608
616
  }
609
- if (config.polyfill?.process) {
610
- plugins.push(new webpack.ProvidePlugin({ process: 'process/browser.js' }));
617
+ if (config.detectCircularDependencies) {
618
+ let circularPluginOptions = {
619
+ exclude: /node_modules/,
620
+ allowAsyncCycles: true,
621
+ };
622
+ if (typeof config.detectCircularDependencies === 'object') {
623
+ circularPluginOptions = config.detectCircularDependencies;
624
+ }
625
+ plugins.push(new circular_dependency_plugin_1.default(circularPluginOptions));
611
626
  }
612
627
  if (isEnvProduction) {
613
- plugins.push(new mini_css_extract_plugin_1.default({
614
- filename: 'css/[name].[contenthash:8].css',
615
- chunkFilename: 'css/[name].[contenthash:8].chunk.css',
616
- ignoreOrder: true,
617
- }));
618
- if (config.sentryConfig) {
619
- const sentryPlugin = require('@sentry/webpack-plugin').sentryWebpackPlugin;
620
- plugins.push(sentryPlugin({ ...config.sentryConfig }));
621
- }
622
628
  if (config.analyzeBundle === 'true') {
623
629
  plugins.push(new webpack_bundle_analyzer_1.BundleAnalyzerPlugin({
624
630
  openAnalyzer: false,
@@ -629,8 +635,8 @@ function configurePlugins(options) {
629
635
  if (config.analyzeBundle === 'statoscope') {
630
636
  const customStatoscopeConfig = config.statoscopeConfig || {};
631
637
  plugins.push(new webpack_plugin_1.default({
632
- saveReportTo: path.resolve(paths_1.default.appBuild, 'report.html'),
633
- saveStatsTo: path.resolve(paths_1.default.appBuild, 'stats.json'),
638
+ saveReportTo: path.resolve(options.buildDirectory, 'report.html'),
639
+ saveStatsTo: path.resolve(options.buildDirectory, 'stats.json'),
634
640
  open: false,
635
641
  statsOptions: {
636
642
  all: true,
@@ -639,50 +645,102 @@ function configurePlugins(options) {
639
645
  }));
640
646
  }
641
647
  }
642
- if (config.cdn) {
643
- let credentialsGlobal;
644
- if (process.env.FRONTEND_S3_ACCESS_KEY_ID && process.env.FRONTEND_S3_SECRET_ACCESS_KEY) {
645
- credentialsGlobal = {
646
- accessKeyId: process.env.FRONTEND_S3_ACCESS_KEY_ID,
647
- secretAccessKey: process.env.FRONTEND_S3_SECRET_ACCESS_KEY,
648
- };
648
+ if (isEnvProduction || isSsr) {
649
+ plugins.push(new mini_css_extract_plugin_1.default({
650
+ filename: 'css/[name].[contenthash:8].css',
651
+ chunkFilename: 'css/[name].[contenthash:8].chunk.css',
652
+ ignoreOrder: true,
653
+ }));
654
+ }
655
+ if (config.monaco) {
656
+ const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin');
657
+ plugins.push(new MonacoEditorWebpackPlugin({
658
+ filename: isEnvProduction ? '[name].[hash:8].worker.js' : undefined,
659
+ ...config.monaco,
660
+ // currently, workers located on cdn are not working properly, so we are enforcing loading workers from
661
+ // service instead
662
+ publicPath: path.normalize(config.publicPathPrefix + '/build/'),
663
+ }));
664
+ }
665
+ if (!isSsr) {
666
+ const contextReplacement = config.contextReplacement || {};
667
+ plugins.push(createMomentTimezoneDataPlugin(config.momentTz));
668
+ plugins.push(new webpack.ContextReplacementPlugin(/moment[\\/]locale$/,
669
+ // eslint-disable-next-line security/detect-non-literal-regexp
670
+ new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})$`)));
671
+ plugins.push(new webpack.ContextReplacementPlugin(/dayjs[\\/]locale$/,
672
+ // eslint-disable-next-line security/detect-non-literal-regexp
673
+ new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})\\.js$`)));
674
+ if (contextReplacement['highlight.js']) {
675
+ plugins.push(new webpack.ContextReplacementPlugin(/highlight\.js[\\/]lib[\\/]languages$/,
676
+ // eslint-disable-next-line security/detect-non-literal-regexp
677
+ new RegExp(`^\\./(${contextReplacement['highlight.js'].join('|')})$`)));
649
678
  }
650
- const cdns = Array.isArray(config.cdn) ? config.cdn : [config.cdn];
651
- for (let index = 0; index < cdns.length; index++) {
652
- const cdn = cdns[index];
653
- if (!cdn) {
654
- continue;
679
+ if (isEnvDevelopment && config.reactRefresh !== false) {
680
+ const { webSocketPath = path.normalize(`/${config.publicPathPrefix}/build/sockjs-node`), } = config.devServer || {};
681
+ plugins.push(new react_refresh_webpack_plugin_1.default(config.reactRefresh({
682
+ overlay: { sockPath: webSocketPath },
683
+ exclude: [/node_modules/, /\.worker\.[jt]sx?$/],
684
+ })));
685
+ }
686
+ if (config.polyfill?.process) {
687
+ plugins.push(new webpack.ProvidePlugin({ process: 'process/browser.js' }));
688
+ }
689
+ if (isEnvProduction) {
690
+ if (config.sentryConfig) {
691
+ const sentryPlugin = require('@sentry/webpack-plugin').sentryWebpackPlugin;
692
+ plugins.push(sentryPlugin({ ...config.sentryConfig }));
655
693
  }
656
- let credentials = credentialsGlobal;
657
- const accessKeyId = process.env[`FRONTEND_S3_ACCESS_KEY_ID_${index}`];
658
- const secretAccessKey = process.env[`FRONTEND_S3_SECRET_ACCESS_KEY_${index}`];
659
- if (accessKeyId && secretAccessKey) {
660
- credentials = {
661
- accessKeyId,
662
- secretAccessKey,
694
+ }
695
+ if (config.cdn) {
696
+ let credentialsGlobal;
697
+ if (process.env.FRONTEND_S3_ACCESS_KEY_ID &&
698
+ process.env.FRONTEND_S3_SECRET_ACCESS_KEY) {
699
+ credentialsGlobal = {
700
+ accessKeyId: process.env.FRONTEND_S3_ACCESS_KEY_ID,
701
+ secretAccessKey: process.env.FRONTEND_S3_SECRET_ACCESS_KEY,
663
702
  };
664
703
  }
665
- plugins.push(new s3_upload_1.S3UploadPlugin({
666
- exclude: config.hiddenSourceMap ? /\.map$/ : undefined,
667
- compress: cdn.compress,
668
- s3ClientOptions: {
669
- region: cdn.region,
670
- endpoint: cdn.endpoint,
671
- credentials,
672
- },
673
- s3UploadOptions: {
674
- bucket: cdn.bucket,
675
- targetPath: cdn.prefix,
676
- cacheControl: cdn.cacheControl,
677
- },
678
- additionalPattern: cdn.additionalPattern,
679
- logger: options.logger,
680
- }));
704
+ const cdns = Array.isArray(config.cdn) ? config.cdn : [config.cdn];
705
+ for (let index = 0; index < cdns.length; index++) {
706
+ const cdn = cdns[index];
707
+ if (!cdn) {
708
+ continue;
709
+ }
710
+ let credentials = credentialsGlobal;
711
+ const accessKeyId = process.env[`FRONTEND_S3_ACCESS_KEY_ID_${index}`];
712
+ const secretAccessKey = process.env[`FRONTEND_S3_SECRET_ACCESS_KEY_${index}`];
713
+ if (accessKeyId && secretAccessKey) {
714
+ credentials = {
715
+ accessKeyId,
716
+ secretAccessKey,
717
+ };
718
+ }
719
+ plugins.push(new s3_upload_1.S3UploadPlugin({
720
+ exclude: config.hiddenSourceMap ? /\.map$/ : undefined,
721
+ compress: cdn.compress,
722
+ s3ClientOptions: {
723
+ region: cdn.region,
724
+ endpoint: cdn.endpoint,
725
+ credentials,
726
+ },
727
+ s3UploadOptions: {
728
+ bucket: cdn.bucket,
729
+ targetPath: cdn.prefix,
730
+ cacheControl: cdn.cacheControl,
731
+ },
732
+ additionalPattern: cdn.additionalPattern,
733
+ logger: options.logger,
734
+ }));
735
+ }
681
736
  }
682
737
  }
683
738
  return plugins;
684
739
  }
685
- function configureOptimization({ config }) {
740
+ function configureOptimization({ config, isSsr }) {
741
+ if (isSsr) {
742
+ return {};
743
+ }
686
744
  const configVendors = config.vendors ?? [];
687
745
  let vendorsList = [
688
746
  'react',
@@ -1,3 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- __webpack_public_path__ = window.__PUBLIC_PATH__ ?? '/build/';
3
+ // @ts-expect-error
4
+ __webpack_public_path__ = globalThis.__PUBLIC_PATH__ ?? '/build/';
@@ -35,6 +35,7 @@ const css_minimizer_webpack_plugin_1 = __importDefault(require("css-minimizer-we
35
35
  const config_1 = require("./config");
36
36
  const config_2 = require("../config");
37
37
  const models_1 = require("../models");
38
+ const paths_1 = __importDefault(require("../paths"));
38
39
  async function configureServiceWebpackConfig(mode, storybookConfig) {
39
40
  const serviceConfig = await (0, config_2.getProjectConfig)(mode === "production" /* WebpackMode.Prod */ ? 'build' : 'dev', {
40
41
  storybook: true,
@@ -104,6 +105,9 @@ async function configureWebpackConfigForStorybook(mode, userConfig = {}, storybo
104
105
  isEnvProduction,
105
106
  config: config.client,
106
107
  configType: mode,
108
+ buildDirectory: paths_1.default.appBuild,
109
+ entriesDirectory: paths_1.default.appEntry,
110
+ isSsr: false,
107
111
  };
108
112
  return {
109
113
  module: {
@@ -1,6 +1,6 @@
1
1
  import type webpack from 'webpack';
2
2
  import type { Logger } from '../logger';
3
- export declare function webpackCompilerHandlerFactory(logger: Logger, onCompilationEnd?: () => void): (err?: Error | null, stats?: webpack.Stats) => Promise<void>;
3
+ export declare function webpackCompilerHandlerFactory(logger: Logger, onCompilationEnd?: () => void): (err?: Error | null, stats?: webpack.MultiStats) => Promise<void>;
4
4
  export declare function resolveTsconfigPathsToAlias(tsConfigPath: string): {
5
5
  aliases: Record<string, string[]>;
6
6
  modules: string[];
@@ -50,13 +50,21 @@ function webpackCompilerHandlerFactory(logger, onCompilationEnd) {
50
50
  if (onCompilationEnd) {
51
51
  await onCompilationEnd();
52
52
  }
53
- if (stats) {
54
- const time = stats.endTime - stats.startTime;
53
+ const [clientStats, ssrStats] = stats?.stats ?? [];
54
+ if (clientStats) {
55
+ const time = clientStats.endTime - clientStats.startTime;
55
56
  logger.success(`Client was successfully compiled in ${(0, pretty_time_1.prettyTime)(BigInt(time) * BigInt(1_000_000))}`);
56
57
  }
57
58
  else {
58
59
  logger.success(`Client was successfully compiled`);
59
60
  }
61
+ if (ssrStats) {
62
+ const time = ssrStats.endTime - ssrStats.startTime;
63
+ logger.success(`SSR: Client was successfully compiled in ${(0, pretty_time_1.prettyTime)(BigInt(time) * BigInt(1_000_000))}`);
64
+ }
65
+ else {
66
+ logger.success(`SSR: Client was successfully compiled`);
67
+ }
60
68
  };
61
69
  }
62
70
  const endStarRe = /\/?\*$/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/app-builder",
3
- "version": "0.13.0",
3
+ "version": "0.14.0-beta.0",
4
4
  "description": "Develop and build your React client-server projects, powered by typescript and webpack",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -129,6 +129,7 @@
129
129
  "webpack-bundle-analyzer": "^4.10.2",
130
130
  "webpack-dev-server": "^5.1.0",
131
131
  "webpack-manifest-plugin": "^5.0.0",
132
+ "webpack-node-externals": "^3.0.0",
132
133
  "worker-loader": "^3.0.8",
133
134
  "yargs": "^17.7.2"
134
135
  },
@@ -152,6 +153,7 @@
152
153
  "@types/webpack-assets-manifest": "^5.1.4",
153
154
  "@types/webpack-bundle-analyzer": "^4.7.0",
154
155
  "@types/webpack-manifest-plugin": "^3.0.8",
156
+ "@types/webpack-node-externals": "^3.0.4",
155
157
  "@types/yargs": "17.0.11",
156
158
  "babel-plugin-tester": "^11.0.4",
157
159
  "eslint": "^8.57.0",