@gravity-ui/app-builder 0.13.1 → 0.14.0-beta.1

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.
package/README.md CHANGED
@@ -192,8 +192,11 @@ With this `{rootDir}/src/ui/tsconfig.json`:
192
192
  - `forkTsCheker` (`false | ForkTsCheckerWebpackPluginOptions`) - config for ForkTsCheckerWebpackPlugin [more](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#options). If `false`, ForkTsCheckerWebpackPlugin will be disabled.
193
193
  - `cache` (`boolean | FileCacheOptions | MemoryCacheOptions`) — Cache the generated webpack modules and chunks to improve build speed. [more](https://webpack.js.org/configuration/cache/)
194
194
  - `babelCacheDirectory` (`boolean | string`) — Set directory for babel-loader cache (`default: node_modules/.cache/babel-loader``)
195
- - `babel` (`(config: babel.TransformOptions, options: {configType: 'development' | 'production'}) => babel.TransformOptions | Promise<babel.TransformOptions>`) - Allow override the default babel transform options.
196
- - `webpack` (`(config: webpack.Configuration, options: {configType: 'development' | 'production'}) => webpack.Configuration | Promise<webpack.Configuration>`) - Allow override the default configuration.
195
+ - `babel` (`(config: babel.TransformOptions, options: {configType: 'development' | 'production'; isSsr: boolean}) => babel.TransformOptions | Promise<babel.TransformOptions>`) - Allow override the default babel transform options.
196
+ - `webpack` (`(config: webpack.Configuration, options: {configType: 'development' | 'production'; isSsr: boolean}) => webpack.Configuration | Promise<webpack.Configuration>`) - Allow override the default configuration.
197
+ - `ssr` - build SSR bundle. The SSR entries should be inside `src/ui/ssr` directory and match the client entries.
198
+ - `noExternal` (`string | RegExp | (string | RegExp)[] | true`) - prevent listed dependencies from being externalized for SSR. By default, all dependencies are externalized.
199
+ - `moduleType`: (`'commonjs' | 'esm'`) - library type for the SSR bundle, by default `commonjs`.
197
200
 
198
201
  ##### Dev build
199
202
 
@@ -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}`]
@@ -1,9 +1,13 @@
1
1
  export declare function babelPreset(config: {
2
2
  newJsxTransform?: boolean;
3
+ isSsr?: boolean;
3
4
  }): (string | {
4
5
  env: {
5
6
  modules: boolean;
6
7
  bugfixes: boolean;
8
+ targets: {
9
+ node: string;
10
+ } | undefined;
7
11
  };
8
12
  runtime: {
9
13
  version: string;
@@ -5,7 +5,11 @@ function babelPreset(config) {
5
5
  return [
6
6
  require.resolve('./ui-preset'),
7
7
  {
8
- env: { modules: false, bugfixes: true },
8
+ env: {
9
+ modules: false,
10
+ bugfixes: true,
11
+ targets: config.isSsr ? { node: 'current' } : undefined,
12
+ },
9
13
  runtime: { version: '^7.13.10' },
10
14
  typescript: true,
11
15
  react: {
@@ -180,17 +180,23 @@ export interface ClientConfig {
180
180
  */
181
181
  webpack?: (config: Configuration, options: {
182
182
  configType: `${WebpackMode}`;
183
+ isSsr?: boolean;
183
184
  }) => Configuration | Promise<Configuration>;
184
185
  /**
185
186
  * Modify or return a custom Babel config.
186
187
  */
187
188
  babel?: (config: Babel.TransformOptions, options: {
188
189
  configType: `${WebpackMode}`;
190
+ isSsr: boolean;
189
191
  }) => Babel.TransformOptions | Promise<Babel.TransformOptions>;
190
192
  /**
191
193
  * Modify or return a custom [Terser options](https://github.com/terser/terser#minify-options).
192
194
  */
193
195
  terser?: (options: TerserOptions) => TerserOptions;
196
+ ssr?: {
197
+ noExternal?: string | RegExp | (string | RegExp)[] | true;
198
+ moduleType?: 'commonjs' | 'esm';
199
+ };
194
200
  }
195
201
  export interface CdnUploadConfig {
196
202
  bucket: string;
@@ -230,10 +236,12 @@ export type NormalizedClientConfig = Omit<ClientConfig, 'publicPathPrefix' | 'hi
230
236
  verbose?: boolean;
231
237
  webpack: (config: Configuration, options: {
232
238
  configType: `${WebpackMode}`;
239
+ isSsr: boolean;
233
240
  }) => Configuration | Promise<Configuration>;
234
241
  debugWebpack?: boolean;
235
242
  babel: (config: Babel.TransformOptions, options: {
236
243
  configType: `${WebpackMode}`;
244
+ isSsr: boolean;
237
245
  }) => Babel.TransformOptions | Promise<Babel.TransformOptions>;
238
246
  reactRefresh: NonNullable<ClientConfig['reactRefresh']>;
239
247
  };
@@ -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/],
@@ -92,7 +107,7 @@ async function webpackConfigFactory(webpackMode, config, { logger } = {}) {
92
107
  },
93
108
  cache: config.cache,
94
109
  };
95
- webpackConfig = await config.webpack(webpackConfig, { configType: webpackMode });
110
+ webpackConfig = await config.webpack(webpackConfig, { configType: webpackMode, isSsr });
96
111
  if (config.debugWebpack) {
97
112
  (0, log_config_1.logConfig)('Preview webpack config', webpackConfig);
98
113
  }
@@ -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,61 +208,74 @@ 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
- presets: [(0, babel_1.babelPreset)(config)],
276
+ presets: [(0, babel_1.babelPreset)({ newJsxTransform: config.newJsxTransform, isSsr })],
246
277
  plugins,
247
- }, { configType });
278
+ }, { configType, isSsr });
248
279
  return {
249
280
  loader: require.resolve('babel-loader'),
250
281
  options: {
@@ -339,7 +370,7 @@ function createStylesRule(options) {
339
370
  use: loaders,
340
371
  };
341
372
  }
342
- function getCssLoaders({ isEnvDevelopment, isEnvProduction, config }, additionalRules) {
373
+ function getCssLoaders({ isEnvDevelopment, isEnvProduction, config, isSsr }, additionalRules) {
343
374
  const loaders = [];
344
375
  if (!config.transformCssWithLightningCss) {
345
376
  loaders.push({
@@ -362,27 +393,32 @@ function getCssLoaders({ isEnvDevelopment, isEnvProduction, config }, additional
362
393
  loaders.unshift({
363
394
  loader: require.resolve('css-loader'),
364
395
  options: {
365
- esModule: false,
366
396
  sourceMap: !config.disableSourceMapGeneration,
367
397
  importLoaders,
368
398
  modules: {
369
399
  auto: true,
370
400
  localIdentName: '[name]__[local]--[hash:base64:5]',
371
401
  exportLocalsConvention: 'camelCase',
402
+ exportOnlyLocals: isSsr,
372
403
  },
373
404
  },
374
405
  });
375
406
  if (isEnvProduction) {
376
- loaders.unshift(mini_css_extract_plugin_1.default.loader);
407
+ loaders.unshift({ loader: mini_css_extract_plugin_1.default.loader, options: { emit: !isSsr } });
377
408
  }
378
409
  if (isEnvDevelopment) {
379
- loaders.unshift({
380
- loader: require.resolve('style-loader'),
381
- });
410
+ if (isSsr || config.ssr) {
411
+ loaders.unshift({ loader: mini_css_extract_plugin_1.default.loader, options: { emit: !isSsr } });
412
+ }
413
+ else {
414
+ loaders.unshift({
415
+ loader: require.resolve('style-loader'),
416
+ });
417
+ }
382
418
  }
383
419
  return loaders;
384
420
  }
385
- function createIconsRule({ isEnvProduction, config }, jsLoader) {
421
+ function createIconsRule({ isEnvProduction, config, isSsr }, jsLoader) {
386
422
  const iconIncludes = config.icons || [];
387
423
  return {
388
424
  // eslint-disable-next-line security/detect-unsafe-regex
@@ -419,11 +455,12 @@ function createIconsRule({ isEnvProduction, config }, jsLoader) {
419
455
  generator: {
420
456
  filename: 'assets/images/[name].[contenthash:8][ext]',
421
457
  publicPath: isEnvProduction ? '../' : undefined,
458
+ emit: isSsr ? false : undefined,
422
459
  },
423
460
  }),
424
461
  };
425
462
  }
426
- function createAssetsRules({ isEnvProduction, config }) {
463
+ function createAssetsRules({ isEnvProduction, config, isSsr }) {
427
464
  const imagesRule = {
428
465
  test: /\.(ico|bmp|gif|jpe?g|png|svg)$/,
429
466
  include: [paths_1.default.appClient, ...(config.images || [])],
@@ -435,6 +472,7 @@ function createAssetsRules({ isEnvProduction, config }) {
435
472
  },
436
473
  generator: {
437
474
  filename: 'assets/images/[name].[contenthash:8][ext]',
475
+ emit: isSsr ? false : undefined,
438
476
  },
439
477
  };
440
478
  const fontsRule = {
@@ -448,6 +486,7 @@ function createAssetsRules({ isEnvProduction, config }) {
448
486
  },
449
487
  generator: {
450
488
  filename: 'assets/fonts/[name].[contenthash:8][ext]',
489
+ emit: isSsr ? false : undefined,
451
490
  },
452
491
  };
453
492
  const rules = [imagesRule, fontsRule];
@@ -467,6 +506,7 @@ function createAssetsRules({ isEnvProduction, config }) {
467
506
  generator: {
468
507
  filename: 'assets/images/[name].[contenthash:8][ext]',
469
508
  publicPath: '../',
509
+ emit: isSsr ? false : undefined,
470
510
  },
471
511
  }, {
472
512
  test: /\.(ttf|eot|woff2?)$/,
@@ -481,17 +521,19 @@ function createAssetsRules({ isEnvProduction, config }) {
481
521
  generator: {
482
522
  filename: 'assets/fonts/[name].[contenthash:8][ext]',
483
523
  publicPath: '../',
524
+ emit: isSsr ? false : undefined,
484
525
  },
485
526
  });
486
527
  }
487
528
  return rules;
488
529
  }
489
- function createFallbackRules({ isEnvProduction }) {
530
+ function createFallbackRules({ isEnvProduction, isSsr }) {
490
531
  const rules = [
491
532
  {
492
533
  type: 'asset/resource',
493
534
  generator: {
494
535
  filename: 'assets/[name].[contenthash:8][ext]',
536
+ emit: isSsr ? false : undefined,
495
537
  },
496
538
  exclude: [/\.[jt]sx?$/, /\.json$/, /\.[cm]js$/, /\.ejs$/],
497
539
  },
@@ -506,6 +548,7 @@ function createFallbackRules({ isEnvProduction }) {
506
548
  generator: {
507
549
  filename: 'assets/[name].[contenthash:8][ext]',
508
550
  publicPath: '../',
551
+ emit: isSsr ? false : undefined,
509
552
  },
510
553
  });
511
554
  }
@@ -520,7 +563,7 @@ function createMomentTimezoneDataPlugin(options = {}) {
520
563
  return new moment_timezone_data_webpack_plugin_1.default({ ...options, startYear, endYear });
521
564
  }
522
565
  function configurePlugins(options) {
523
- const { isEnvDevelopment, isEnvProduction, config } = options;
566
+ const { isEnvDevelopment, isEnvProduction, config, isSsr } = options;
524
567
  const excludeFromClean = config.excludeFromClean || [];
525
568
  const manifestFile = 'assets-manifest.json';
526
569
  const plugins = [
@@ -544,11 +587,11 @@ function configurePlugins(options) {
544
587
  : {
545
588
  entrypoints: true,
546
589
  writeToDisk: true,
547
- output: path.resolve(paths_1.default.appBuild, manifestFile),
590
+ output: path.resolve(options.buildDirectory, manifestFile),
548
591
  }),
549
- createMomentTimezoneDataPlugin(config.momentTz),
550
592
  new webpack.DefinePlugin({
551
593
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
594
+ 'process.env.IS_SSR': JSON.stringify(isSsr),
552
595
  ...config.definitions,
553
596
  }),
554
597
  ];
@@ -558,51 +601,12 @@ function configurePlugins(options) {
558
601
  if (process.env.WEBPACK_PROFILE === 'true') {
559
602
  plugins.push(new webpack.debug.ProfilingPlugin());
560
603
  }
561
- const contextReplacement = config.contextReplacement || {};
562
- plugins.push(new webpack.ContextReplacementPlugin(/moment[\\/]locale$/,
563
- // eslint-disable-next-line security/detect-non-literal-regexp
564
- new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})$`)));
565
- plugins.push(new webpack.ContextReplacementPlugin(/dayjs[\\/]locale$/,
566
- // eslint-disable-next-line security/detect-non-literal-regexp
567
- new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})\\.js$`)));
568
- if (contextReplacement['highlight.js']) {
569
- plugins.push(new webpack.ContextReplacementPlugin(/highlight\.js[\\/]lib[\\/]languages$/,
570
- // eslint-disable-next-line security/detect-non-literal-regexp
571
- new RegExp(`^\\./(${contextReplacement['highlight.js'].join('|')})$`)));
572
- }
573
- if (config.monaco) {
574
- const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin');
575
- plugins.push(new MonacoEditorWebpackPlugin({
576
- filename: isEnvProduction ? '[name].[hash:8].worker.js' : undefined,
577
- ...config.monaco,
578
- // currently, workers located on cdn are not working properly, so we are enforcing loading workers from
579
- // service instead
580
- publicPath: path.normalize(config.publicPathPrefix + '/build/'),
581
- }));
582
- }
583
- if (isEnvDevelopment && config.reactRefresh !== false) {
584
- const { webSocketPath = path.normalize(`/${config.publicPathPrefix}/build/sockjs-node`) } = config.devServer || {};
585
- plugins.push(new react_refresh_webpack_plugin_1.default(config.reactRefresh({
586
- overlay: { sockPath: webSocketPath },
587
- exclude: [/node_modules/, /\.worker\.[jt]sx?$/],
588
- })));
589
- }
590
- if (config.detectCircularDependencies) {
591
- let circularPluginOptions = {
592
- exclude: /node_modules/,
593
- allowAsyncCycles: true,
594
- };
595
- if (typeof config.detectCircularDependencies === 'object') {
596
- circularPluginOptions = config.detectCircularDependencies;
597
- }
598
- plugins.push(new circular_dependency_plugin_1.default(circularPluginOptions));
599
- }
600
604
  if (config.forkTsChecker !== false) {
601
605
  plugins.push(new fork_ts_checker_webpack_plugin_1.default({
602
606
  ...config.forkTsChecker,
603
607
  typescript: {
604
608
  typescriptPath: (0, utils_2.resolveTypescript)(),
605
- configFile: path.resolve(paths_1.default.app, 'src/ui/tsconfig.json'),
609
+ configFile: path.resolve(paths_1.default.appClient, 'tsconfig.json'),
606
610
  diagnosticOptions: {
607
611
  syntactic: true,
608
612
  },
@@ -611,19 +615,17 @@ function configurePlugins(options) {
611
615
  },
612
616
  }));
613
617
  }
614
- if (config.polyfill?.process) {
615
- plugins.push(new webpack.ProvidePlugin({ process: 'process/browser.js' }));
618
+ if (config.detectCircularDependencies) {
619
+ let circularPluginOptions = {
620
+ exclude: /node_modules/,
621
+ allowAsyncCycles: true,
622
+ };
623
+ if (typeof config.detectCircularDependencies === 'object') {
624
+ circularPluginOptions = config.detectCircularDependencies;
625
+ }
626
+ plugins.push(new circular_dependency_plugin_1.default(circularPluginOptions));
616
627
  }
617
628
  if (isEnvProduction) {
618
- plugins.push(new mini_css_extract_plugin_1.default({
619
- filename: 'css/[name].[contenthash:8].css',
620
- chunkFilename: 'css/[name].[contenthash:8].chunk.css',
621
- ignoreOrder: true,
622
- }));
623
- if (config.sentryConfig) {
624
- const sentryPlugin = require('@sentry/webpack-plugin').sentryWebpackPlugin;
625
- plugins.push(sentryPlugin({ ...config.sentryConfig }));
626
- }
627
629
  if (config.analyzeBundle === 'true') {
628
630
  plugins.push(new webpack_bundle_analyzer_1.BundleAnalyzerPlugin({
629
631
  openAnalyzer: false,
@@ -634,8 +636,8 @@ function configurePlugins(options) {
634
636
  if (config.analyzeBundle === 'statoscope') {
635
637
  const customStatoscopeConfig = config.statoscopeConfig || {};
636
638
  plugins.push(new webpack_plugin_1.default({
637
- saveReportTo: path.resolve(paths_1.default.appBuild, 'report.html'),
638
- saveStatsTo: path.resolve(paths_1.default.appBuild, 'stats.json'),
639
+ saveReportTo: path.resolve(options.buildDirectory, 'report.html'),
640
+ saveStatsTo: path.resolve(options.buildDirectory, 'stats.json'),
639
641
  open: false,
640
642
  statsOptions: {
641
643
  all: true,
@@ -644,50 +646,104 @@ function configurePlugins(options) {
644
646
  }));
645
647
  }
646
648
  }
647
- if (config.cdn) {
648
- let credentialsGlobal;
649
- if (process.env.FRONTEND_S3_ACCESS_KEY_ID && process.env.FRONTEND_S3_SECRET_ACCESS_KEY) {
650
- credentialsGlobal = {
651
- accessKeyId: process.env.FRONTEND_S3_ACCESS_KEY_ID,
652
- secretAccessKey: process.env.FRONTEND_S3_SECRET_ACCESS_KEY,
653
- };
649
+ if (isEnvProduction || isSsr || config.ssr) {
650
+ plugins.push(new mini_css_extract_plugin_1.default({
651
+ filename: isEnvProduction ? 'css/[name].[contenthash:8].css' : 'css/[name].css',
652
+ chunkFilename: isEnvProduction
653
+ ? 'css/[name].[contenthash:8].chunk.css'
654
+ : 'css/[name].chunk.css',
655
+ ignoreOrder: true,
656
+ }));
657
+ }
658
+ if (!isSsr) {
659
+ if (config.monaco) {
660
+ const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin');
661
+ plugins.push(new MonacoEditorWebpackPlugin({
662
+ filename: isEnvProduction ? '[name].[hash:8].worker.js' : undefined,
663
+ ...config.monaco,
664
+ // currently, workers located on cdn are not working properly, so we are enforcing loading workers from
665
+ // service instead
666
+ publicPath: path.normalize(config.publicPathPrefix + '/build/'),
667
+ }));
654
668
  }
655
- const cdns = Array.isArray(config.cdn) ? config.cdn : [config.cdn];
656
- for (let index = 0; index < cdns.length; index++) {
657
- const cdn = cdns[index];
658
- if (!cdn) {
659
- continue;
669
+ const contextReplacement = config.contextReplacement || {};
670
+ plugins.push(createMomentTimezoneDataPlugin(config.momentTz));
671
+ plugins.push(new webpack.ContextReplacementPlugin(/moment[\\/]locale$/,
672
+ // eslint-disable-next-line security/detect-non-literal-regexp
673
+ new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})$`)));
674
+ plugins.push(new webpack.ContextReplacementPlugin(/dayjs[\\/]locale$/,
675
+ // eslint-disable-next-line security/detect-non-literal-regexp
676
+ new RegExp(`^\\./(${(contextReplacement.locale || ['ru']).join('|')})\\.js$`)));
677
+ if (contextReplacement['highlight.js']) {
678
+ plugins.push(new webpack.ContextReplacementPlugin(/highlight\.js[\\/]lib[\\/]languages$/,
679
+ // eslint-disable-next-line security/detect-non-literal-regexp
680
+ new RegExp(`^\\./(${contextReplacement['highlight.js'].join('|')})$`)));
681
+ }
682
+ if (isEnvDevelopment && config.reactRefresh !== false) {
683
+ const { webSocketPath = path.normalize(`/${config.publicPathPrefix}/build/sockjs-node`), } = config.devServer || {};
684
+ plugins.push(new react_refresh_webpack_plugin_1.default(config.reactRefresh({
685
+ overlay: { sockPath: webSocketPath },
686
+ exclude: [/node_modules/, /\.worker\.[jt]sx?$/],
687
+ })));
688
+ }
689
+ if (config.polyfill?.process) {
690
+ plugins.push(new webpack.ProvidePlugin({ process: 'process/browser.js' }));
691
+ }
692
+ if (isEnvProduction) {
693
+ if (config.sentryConfig) {
694
+ const sentryPlugin = require('@sentry/webpack-plugin').sentryWebpackPlugin;
695
+ plugins.push(sentryPlugin({ ...config.sentryConfig }));
660
696
  }
661
- let credentials = credentialsGlobal;
662
- const accessKeyId = process.env[`FRONTEND_S3_ACCESS_KEY_ID_${index}`];
663
- const secretAccessKey = process.env[`FRONTEND_S3_SECRET_ACCESS_KEY_${index}`];
664
- if (accessKeyId && secretAccessKey) {
665
- credentials = {
666
- accessKeyId,
667
- secretAccessKey,
697
+ }
698
+ if (config.cdn) {
699
+ let credentialsGlobal;
700
+ if (process.env.FRONTEND_S3_ACCESS_KEY_ID &&
701
+ process.env.FRONTEND_S3_SECRET_ACCESS_KEY) {
702
+ credentialsGlobal = {
703
+ accessKeyId: process.env.FRONTEND_S3_ACCESS_KEY_ID,
704
+ secretAccessKey: process.env.FRONTEND_S3_SECRET_ACCESS_KEY,
668
705
  };
669
706
  }
670
- plugins.push(new s3_upload_1.S3UploadPlugin({
671
- exclude: config.hiddenSourceMap ? /\.map$/ : undefined,
672
- compress: cdn.compress,
673
- s3ClientOptions: {
674
- region: cdn.region,
675
- endpoint: cdn.endpoint,
676
- credentials,
677
- },
678
- s3UploadOptions: {
679
- bucket: cdn.bucket,
680
- targetPath: cdn.prefix,
681
- cacheControl: cdn.cacheControl,
682
- },
683
- additionalPattern: cdn.additionalPattern,
684
- logger: options.logger,
685
- }));
707
+ const cdns = Array.isArray(config.cdn) ? config.cdn : [config.cdn];
708
+ for (let index = 0; index < cdns.length; index++) {
709
+ const cdn = cdns[index];
710
+ if (!cdn) {
711
+ continue;
712
+ }
713
+ let credentials = credentialsGlobal;
714
+ const accessKeyId = process.env[`FRONTEND_S3_ACCESS_KEY_ID_${index}`];
715
+ const secretAccessKey = process.env[`FRONTEND_S3_SECRET_ACCESS_KEY_${index}`];
716
+ if (accessKeyId && secretAccessKey) {
717
+ credentials = {
718
+ accessKeyId,
719
+ secretAccessKey,
720
+ };
721
+ }
722
+ plugins.push(new s3_upload_1.S3UploadPlugin({
723
+ exclude: config.hiddenSourceMap ? /\.map$/ : undefined,
724
+ compress: cdn.compress,
725
+ s3ClientOptions: {
726
+ region: cdn.region,
727
+ endpoint: cdn.endpoint,
728
+ credentials,
729
+ },
730
+ s3UploadOptions: {
731
+ bucket: cdn.bucket,
732
+ targetPath: cdn.prefix,
733
+ cacheControl: cdn.cacheControl,
734
+ },
735
+ additionalPattern: cdn.additionalPattern,
736
+ logger: options.logger,
737
+ }));
738
+ }
686
739
  }
687
740
  }
688
741
  return plugins;
689
742
  }
690
- function configureOptimization({ config }) {
743
+ function configureOptimization({ config, isSsr }) {
744
+ if (isSsr) {
745
+ return {};
746
+ }
691
747
  const configVendors = config.vendors ?? [];
692
748
  let vendorsList = [
693
749
  '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,11 +50,16 @@ 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
- else {
58
+ if (ssrStats) {
59
+ const time = ssrStats.endTime - ssrStats.startTime;
60
+ logger.success(`SSR: Client was successfully compiled in ${(0, pretty_time_1.prettyTime)(BigInt(time) * BigInt(1_000_000))}`);
61
+ }
62
+ if (!clientStats && !ssrStats) {
58
63
  logger.success(`Client was successfully compiled`);
59
64
  }
60
65
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/app-builder",
3
- "version": "0.13.1",
3
+ "version": "0.14.0-beta.1",
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",